feat: Customisable Email Templates (#1095)
This commit is contained in:
parent
db877453a4
commit
a04ca24def
27 changed files with 409 additions and 44 deletions
13
Gemfile
13
Gemfile
|
@ -8,7 +8,7 @@ gem 'rails'
|
||||||
# Reduces boot times through caching; required in config/boot.rb
|
# Reduces boot times through caching; required in config/boot.rb
|
||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
|
|
||||||
##-- rails helper gems --##
|
##-- rails application helper gems --##
|
||||||
gem 'acts-as-taggable-on'
|
gem 'acts-as-taggable-on'
|
||||||
gem 'attr_extras'
|
gem 'attr_extras'
|
||||||
gem 'browser'
|
gem 'browser'
|
||||||
|
@ -23,6 +23,12 @@ gem 'tzinfo-data'
|
||||||
gem 'valid_email2'
|
gem 'valid_email2'
|
||||||
# compress javascript config.assets.js_compressor
|
# compress javascript config.assets.js_compressor
|
||||||
gem 'uglifier'
|
gem 'uglifier'
|
||||||
|
##-- used for single column multiple binary flags in notification settings/feature flagging --##
|
||||||
|
gem 'flag_shih_tzu'
|
||||||
|
# Random name generator for user names
|
||||||
|
gem 'haikunator'
|
||||||
|
# Template parsing safetly
|
||||||
|
gem 'liquid'
|
||||||
|
|
||||||
##-- for active storage --##
|
##-- for active storage --##
|
||||||
gem 'aws-sdk-s3', require: false
|
gem 'aws-sdk-s3', require: false
|
||||||
|
@ -67,8 +73,6 @@ gem 'twitty'
|
||||||
gem 'koala'
|
gem 'koala'
|
||||||
# slack client
|
# slack client
|
||||||
gem 'slack-ruby-client'
|
gem 'slack-ruby-client'
|
||||||
# Random name generator
|
|
||||||
gem 'haikunator'
|
|
||||||
|
|
||||||
##--- gems for debugging and error reporting ---##
|
##--- gems for debugging and error reporting ---##
|
||||||
# static analysis
|
# static analysis
|
||||||
|
@ -79,9 +83,6 @@ gem 'sentry-raven'
|
||||||
##-- background job processing --##
|
##-- background job processing --##
|
||||||
gem 'sidekiq'
|
gem 'sidekiq'
|
||||||
|
|
||||||
##-- used for single column multiple binary flags in notification settings/feature flagging --##
|
|
||||||
gem 'flag_shih_tzu'
|
|
||||||
|
|
||||||
##-- Push notification service --##
|
##-- Push notification service --##
|
||||||
gem 'fcm'
|
gem 'fcm'
|
||||||
gem 'webpush'
|
gem 'webpush'
|
||||||
|
|
|
@ -275,6 +275,7 @@ GEM
|
||||||
addressable (~> 2.7)
|
addressable (~> 2.7)
|
||||||
letter_opener (1.7.0)
|
letter_opener (1.7.0)
|
||||||
launchy (~> 2.2)
|
launchy (~> 2.2)
|
||||||
|
liquid (4.0.3)
|
||||||
listen (3.2.1)
|
listen (3.2.1)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
|
@ -584,6 +585,7 @@ DEPENDENCIES
|
||||||
kaminari
|
kaminari
|
||||||
koala
|
koala
|
||||||
letter_opener
|
letter_opener
|
||||||
|
liquid
|
||||||
listen
|
listen
|
||||||
mini_magick
|
mini_magick
|
||||||
mock_redis!
|
mock_redis!
|
||||||
|
|
2
app/drops/account_drop.rb
Normal file
2
app/drops/account_drop.rb
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
class AccountDrop < BaseDrop
|
||||||
|
end
|
13
app/drops/base_drop.rb
Normal file
13
app/drops/base_drop.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
class BaseDrop < Liquid::Drop
|
||||||
|
def initialize(obj)
|
||||||
|
@obj = obj
|
||||||
|
end
|
||||||
|
|
||||||
|
def id
|
||||||
|
@obj.try(:id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def name
|
||||||
|
@obj.try(:name)
|
||||||
|
end
|
||||||
|
end
|
5
app/drops/conversation_drop.rb
Normal file
5
app/drops/conversation_drop.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
class ConversationDrop < BaseDrop
|
||||||
|
def display_id
|
||||||
|
@obj.try(:display_id)
|
||||||
|
end
|
||||||
|
end
|
2
app/drops/inbox_drop.rb
Normal file
2
app/drops/inbox_drop.rb
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
class InboxDrop < BaseDrop
|
||||||
|
end
|
2
app/drops/user_drop.rb
Normal file
2
app/drops/user_drop.rb
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
class UserDrop < BaseDrop
|
||||||
|
end
|
|
@ -1,14 +1,12 @@
|
||||||
class AgentNotifications::ConversationNotificationsMailer < ApplicationMailer
|
class AgentNotifications::ConversationNotificationsMailer < ApplicationMailer
|
||||||
default from: ENV.fetch('MAILER_SENDER_EMAIL', 'accounts@chatwoot.com')
|
|
||||||
layout 'mailer'
|
|
||||||
|
|
||||||
def conversation_creation(conversation, agent)
|
def conversation_creation(conversation, agent)
|
||||||
return unless smtp_config_set_or_development?
|
return unless smtp_config_set_or_development?
|
||||||
|
|
||||||
@agent = agent
|
@agent = agent
|
||||||
@conversation = conversation
|
@conversation = conversation
|
||||||
subject = "#{@agent.available_name}, A new conversation [ID - #{@conversation.display_id}] has been created in #{@conversation.inbox&.name}."
|
subject = "#{@agent.available_name}, A new conversation [ID - #{@conversation.display_id}] has been created in #{@conversation.inbox&.name}."
|
||||||
mail(to: @agent.email, subject: subject)
|
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
|
||||||
|
send_mail_with_liquid(to: @agent.email, subject: subject) and return
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation_assignment(conversation, agent)
|
def conversation_assignment(conversation, agent)
|
||||||
|
@ -16,6 +14,18 @@ class AgentNotifications::ConversationNotificationsMailer < ApplicationMailer
|
||||||
|
|
||||||
@agent = agent
|
@agent = agent
|
||||||
@conversation = conversation
|
@conversation = conversation
|
||||||
mail(to: @agent.email, subject: "#{@agent.available_name}, A new conversation [ID - #{@conversation.display_id}] has been assigned to you.")
|
subject = "#{@agent.available_name}, A new conversation [ID - #{@conversation.display_id}] has been assigned to you."
|
||||||
|
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
|
||||||
|
send_mail_with_liquid(to: @agent.email, subject: subject) and return
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def liquid_droppables
|
||||||
|
super.merge({
|
||||||
|
user: @agent,
|
||||||
|
conversation: @conversation,
|
||||||
|
inbox: @conversation.inbox
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
class ApplicationMailer < ActionMailer::Base
|
class ApplicationMailer < ActionMailer::Base
|
||||||
default from: ENV.fetch('MAILER_SENDER_EMAIL', 'accounts@chatwoot.com')
|
include ActionView::Helpers::SanitizeHelper
|
||||||
layout 'mailer'
|
|
||||||
append_view_path Rails.root.join('app/views/mailers')
|
|
||||||
|
|
||||||
# helpers
|
default from: ENV.fetch('MAILER_SENDER_EMAIL', 'accounts@chatwoot.com')
|
||||||
|
before_action { ensure_current_account(params.try(:[], :account)) }
|
||||||
|
layout 'mailer/base'
|
||||||
|
# Fetch template from Database if available
|
||||||
|
# Order: Account Specific > Installation Specific > Fallback to file
|
||||||
|
prepend_view_path ::EmailTemplate.resolver
|
||||||
|
append_view_path Rails.root.join('app/views/mailers')
|
||||||
helper :frontend_urls
|
helper :frontend_urls
|
||||||
helper do
|
helper do
|
||||||
def global_config
|
def global_config
|
||||||
|
@ -14,4 +18,36 @@ class ApplicationMailer < ActionMailer::Base
|
||||||
def smtp_config_set_or_development?
|
def smtp_config_set_or_development?
|
||||||
ENV.fetch('SMTP_ADDRESS', nil).present? || Rails.env.development?
|
ENV.fetch('SMTP_ADDRESS', nil).present? || Rails.env.development?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def send_mail_with_liquid(*args)
|
||||||
|
mail(*args) do |format|
|
||||||
|
# explored sending a multipart email containg both text type and html
|
||||||
|
# parsing the html with nokogiri will remove the links as well
|
||||||
|
# might also remove tags like b,li etc. so lets rethink about this later
|
||||||
|
# format.text { Nokogiri::HTML(render(layout: false)).text }
|
||||||
|
format.html { render }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def liquid_droppables
|
||||||
|
# Merge additional objects into this in your mailer
|
||||||
|
# liquid template handler converts these objects into drop objects
|
||||||
|
{
|
||||||
|
account: Current.account
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def liquid_locals
|
||||||
|
# expose variables you want to be exposed in liquid
|
||||||
|
{
|
||||||
|
global_config: GlobalConfig.get('INSTALLATION_NAME', 'BRAND_URL'),
|
||||||
|
action_url: @action_url
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_current_account(account)
|
||||||
|
Current.account = account if account.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -105,6 +105,6 @@ class ConversationReplyMailer < ApplicationMailer
|
||||||
def choose_layout
|
def choose_layout
|
||||||
return false if action_name == 'reply_without_summary'
|
return false if action_name == 'reply_without_summary'
|
||||||
|
|
||||||
'mailer'
|
'mailer/base'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
class ApplicationRecord < ActiveRecord::Base
|
class ApplicationRecord < ActiveRecord::Base
|
||||||
self.abstract_class = true
|
self.abstract_class = true
|
||||||
|
DROPPABLES = %w[Account Channel Conversation Inbox User].freeze
|
||||||
|
|
||||||
|
def to_drop
|
||||||
|
return unless DROPPABLES.include?(self.class.name)
|
||||||
|
|
||||||
|
"#{self.class.name}Drop".constantize.new(self)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -52,8 +52,10 @@ class Conversation < ApplicationRecord
|
||||||
|
|
||||||
before_create :set_display_id, unless: :display_id?
|
before_create :set_display_id, unless: :display_id?
|
||||||
before_create :set_bot_conversation
|
before_create :set_bot_conversation
|
||||||
after_create :notify_conversation_creation
|
after_create_commit :notify_conversation_creation
|
||||||
after_save :run_round_robin
|
after_save :run_round_robin
|
||||||
|
# wanted to change this to after_update commit. But it ended up creating a loop
|
||||||
|
# reinvestigate in future and identity the implications
|
||||||
after_update :notify_status_change, :create_activity
|
after_update :notify_status_change, :create_activity
|
||||||
|
|
||||||
acts_as_taggable_on :labels
|
acts_as_taggable_on :labels
|
||||||
|
|
28
app/models/email_template.rb
Normal file
28
app/models/email_template.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: email_templates
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# body :text not null
|
||||||
|
# locale :integer default("en"), not null
|
||||||
|
# name :string not null
|
||||||
|
# template_type :integer default("content")
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :integer
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_email_templates_on_name_and_account_id (name,account_id) UNIQUE
|
||||||
|
#
|
||||||
|
class EmailTemplate < ApplicationRecord
|
||||||
|
enum locale: LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h
|
||||||
|
enum template_type: { layout: 0, content: 1 }
|
||||||
|
belongs_to :account, optional: true
|
||||||
|
|
||||||
|
validates :name, uniqueness: { scope: :account }
|
||||||
|
|
||||||
|
def self.resolver(options = {})
|
||||||
|
::EmailTemplates::DbResolverService.using self, options
|
||||||
|
end
|
||||||
|
end
|
87
app/services/email_templates/db_resolver_service.rb
Normal file
87
app/services/email_templates/db_resolver_service.rb
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
# Code is heavily inspired by panaromic gem
|
||||||
|
# https://github.com/andreapavoni/panoramic
|
||||||
|
# We will try to find layouts and content from database
|
||||||
|
# layout will be rendered with erb and other content in html format
|
||||||
|
# Further processing in liquid is implemented in mailers
|
||||||
|
|
||||||
|
# Note: rails resolver looks for templates in cache first
|
||||||
|
# which we don't want to happen here
|
||||||
|
# so we are overriding find_all method in action view resolver
|
||||||
|
# If anything breaks - look into rails : actionview/lib/action_view/template/resolver.rb
|
||||||
|
|
||||||
|
class ::EmailTemplates::DbResolverService < ActionView::Resolver
|
||||||
|
require 'singleton'
|
||||||
|
include Singleton
|
||||||
|
|
||||||
|
# Instantiate Resolver by passing a model.
|
||||||
|
def self.using(model, options = {})
|
||||||
|
class_variable_set(:@@model, model)
|
||||||
|
class_variable_set(:@@resolver_options, options)
|
||||||
|
instance
|
||||||
|
end
|
||||||
|
|
||||||
|
# Since rails picks up files from cache. lets override the method
|
||||||
|
# Normalizes the arguments and passes it on to find_templates.
|
||||||
|
# rubocop:disable Metrics/ParameterLists
|
||||||
|
def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = [])
|
||||||
|
locals = locals.map(&:to_s).sort!.freeze
|
||||||
|
_find_all(name, prefix, partial, details, key, locals)
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics/ParameterLists
|
||||||
|
|
||||||
|
# the function has to accept(name, prefix, partial, _details, _locals = [])
|
||||||
|
# _details contain local info which we can leverage in future
|
||||||
|
# cause of codeclimate issue with 4 args, relying on (*args)
|
||||||
|
def find_templates(name, prefix, partial, *_args)
|
||||||
|
@template_name = name
|
||||||
|
@template_type = prefix.include?('layout') ? 'layout' : 'content'
|
||||||
|
@db_template = find_db_template
|
||||||
|
|
||||||
|
return [] if @db_template.blank?
|
||||||
|
|
||||||
|
path = build_path(prefix)
|
||||||
|
handler = ActionView::Template.registered_template_handler(:liquid)
|
||||||
|
|
||||||
|
template_details = {
|
||||||
|
format: Mime['html'].to_sym,
|
||||||
|
updated_at: @db_template.updated_at,
|
||||||
|
virtual_path: virtual_path(path, partial)
|
||||||
|
}
|
||||||
|
|
||||||
|
[ActionView::Template.new(@db_template.body, "DB Template - #{@db_template.id}", handler, template_details)]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_db_template
|
||||||
|
find_account_template || find_installation_template
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_account_template
|
||||||
|
return unless Current.account
|
||||||
|
|
||||||
|
@@model.find_by(name: @template_name, template_type: @template_type, account: Current.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_installation_template
|
||||||
|
@@model.find_by(name: @template_name, template_type: @template_type, account: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Build path with eventual prefix
|
||||||
|
def build_path(prefix)
|
||||||
|
prefix.present? ? "#{prefix}/#{@template_name}" : @template_name
|
||||||
|
end
|
||||||
|
|
||||||
|
# returns a path depending if its a partial or template
|
||||||
|
# params path: path/to/file.ext partial: true/false
|
||||||
|
# the function appends _to make the file name _file.ext if partial: true
|
||||||
|
def virtual_path(path, partial)
|
||||||
|
return path unless partial
|
||||||
|
|
||||||
|
if (index = path.rindex('/'))
|
||||||
|
path.insert(index + 1, '_')
|
||||||
|
else
|
||||||
|
"_#{path}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,7 +8,7 @@ class Notification::EmailNotificationService
|
||||||
|
|
||||||
# TODO : Clean up whatever happening over here
|
# TODO : Clean up whatever happening over here
|
||||||
# Segregate the mailers properly
|
# Segregate the mailers properly
|
||||||
AgentNotifications::ConversationNotificationsMailer.public_send(notification
|
AgentNotifications::ConversationNotificationsMailer.with(account: notification.account).public_send(notification
|
||||||
.notification_type.to_s, notification.primary_actor, notification.user).deliver_now
|
.notification_type.to_s, notification.primary_actor, notification.user).deliver_now
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,7 @@
|
||||||
<td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
|
<td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
|
||||||
<meta itemprop="name" content="Confirm Email" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
|
<meta itemprop="name" content="Confirm Email" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
<table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||||
<%= yield %>
|
{{ content_for_layout }}
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -89,16 +89,16 @@
|
||||||
<div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
|
<div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
|
||||||
<table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
<table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||||
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||||
<% if global_config['BRAND_NAME'].present? %>
|
{% if global_config['BRAND_NAME'] %}
|
||||||
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||||
<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
|
<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
|
||||||
Powered by
|
Powered by
|
||||||
<a href="<%= global_config['BRAND_URL'] %>" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">
|
<a href="{{ global_config['BRAND_URL'] }}" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">
|
||||||
<%= global_config['BRAND_NAME'] %>
|
{{ global_config['BRAND_NAME'] }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
|
@ -1,10 +0,0 @@
|
||||||
<p>Hi <%= @agent.available_name %>,</p>
|
|
||||||
|
|
||||||
<p>Time to save the world. A new conversation has been assigned to you</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Click <%=
|
|
||||||
link_to 'here',
|
|
||||||
app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
|
|
||||||
%> to get cracking.
|
|
||||||
</p>
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<p>Hi {{user.available_name}},</p>
|
||||||
|
|
||||||
|
<p>Time to save the world. A new conversation has been assigned to you</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Click <a href="{{action_url}}">here</a> to get cracking.
|
||||||
|
</p>
|
|
@ -1,10 +0,0 @@
|
||||||
<p>Hi <%= @agent.available_name %>,</p>
|
|
||||||
|
|
||||||
<p>Time to save the world. A new conversation has been created in <%= @conversation.inbox.name %></p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Click <%=
|
|
||||||
link_to 'here',
|
|
||||||
app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
|
|
||||||
%> to get cracking.
|
|
||||||
</p>
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<p>Hi {{user.available_name}}</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p>Time to save the world. A new conversation has been created in {{ inbox.name }}</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Click <a href="{{ action_url }}">here</a> to get cracking.
|
||||||
|
</p>
|
1
config/initializers/liquid_handler.rb
Normal file
1
config/initializers/liquid_handler.rb
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ActionView::Template.register_template_handler :liquid, ActionView::Template::Handlers::Liquid
|
13
db/migrate/20200725131651_create_email_templates.rb
Normal file
13
db/migrate/20200725131651_create_email_templates.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
class CreateEmailTemplates < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
create_table :email_templates do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.text :body, null: false
|
||||||
|
t.integer :account_id, null: true
|
||||||
|
t.integer :template_type, default: 1
|
||||||
|
t.integer :locale, default: 0, null: false
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
add_index :email_templates, [:name, :account_id], unique: true
|
||||||
|
end
|
||||||
|
end
|
11
db/schema.rb
11
db/schema.rb
|
@ -231,6 +231,17 @@ ActiveRecord::Schema.define(version: 2020_08_02_170002) do
|
||||||
t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id"
|
t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "email_templates", force: :cascade do |t|
|
||||||
|
t.string "name", null: false
|
||||||
|
t.text "body", null: false
|
||||||
|
t.integer "account_id"
|
||||||
|
t.integer "template_type", default: 1
|
||||||
|
t.integer "locale", default: 0, null: false
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["name", "account_id"], name: "index_email_templates_on_name_and_account_id", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "events", force: :cascade do |t|
|
create_table "events", force: :cascade do |t|
|
||||||
t.string "name"
|
t.string "name"
|
||||||
t.float "value"
|
t.float "value"
|
||||||
|
|
62
lib/action_view/template/handlers/liquid.rb
Normal file
62
lib/action_view/template/handlers/liquid.rb
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# Code inspired by
|
||||||
|
# http://royvandermeij.com/blog/2011/09/21/create-a-liquid-handler-for-rails-3-dot-1/
|
||||||
|
# https://github.com/chamnap/liquid-rails/blob/master/lib/liquid-rails/template_handler.rb
|
||||||
|
|
||||||
|
class ActionView::Template::Handlers::Liquid
|
||||||
|
def self.call(template, _source)
|
||||||
|
"ActionView::Template::Handlers::Liquid.new(self).render(#{template.source.inspect}, local_assigns)"
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(view)
|
||||||
|
@view = view
|
||||||
|
@controller = @view.controller
|
||||||
|
@helper = ActionController::Base.helpers
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(template, local_assigns = {})
|
||||||
|
assigns = drops
|
||||||
|
assigns['content_for_layout'] = @view.content_for(:layout) if @view.content_for?(:layout)
|
||||||
|
assigns.merge!(local_assigns)
|
||||||
|
assigns.merge!(locals)
|
||||||
|
|
||||||
|
liquid = Liquid::Template.parse(template)
|
||||||
|
liquid.send(render_method, assigns.stringify_keys, filters: filters, registers: registers.stringify_keys)
|
||||||
|
end
|
||||||
|
|
||||||
|
def locals
|
||||||
|
if @controller.respond_to?(:liquid_locals, true)
|
||||||
|
@controller.send(:liquid_locals)
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def drops
|
||||||
|
droppables = @controller.send(:liquid_droppables) if @controller.respond_to?(:liquid_droppables, true)
|
||||||
|
droppables.update(droppables) { |_, obj| obj.try(:to_drop) || nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
def filters
|
||||||
|
if @controller.respond_to?(:liquid_filters, true)
|
||||||
|
@controller.send(:liquid_filters)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def registers
|
||||||
|
if @controller.respond_to?(:liquid_registers, true)
|
||||||
|
@controller.send(:liquid_registers)
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def compilable?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_method
|
||||||
|
::Rails.env.development? || ::Rails.env.test? ? :render! : :render
|
||||||
|
end
|
||||||
|
end
|
5
spec/factories/email_template.rb
Normal file
5
spec/factories/email_template.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :email_template do
|
||||||
|
name { 'MyString' }
|
||||||
|
end
|
||||||
|
end
|
80
spec/lib/email_templates/db_resolver_service_spec.rb
Normal file
80
spec/lib/email_templates/db_resolver_service_spec.rb
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe ::EmailTemplates::DbResolverService do
|
||||||
|
subject(:resolver) { described_class.using(EmailTemplate, {}) }
|
||||||
|
|
||||||
|
describe '#find_templates' do
|
||||||
|
context 'when template does not exist in db' do
|
||||||
|
it 'return empty array' do
|
||||||
|
expect(resolver.find_templates('test', '', false, [])).to eq([])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when installation template exist in db' do
|
||||||
|
it 'return installation template' do
|
||||||
|
email_template = create(:email_template, name: 'test', body: 'test')
|
||||||
|
handler = ActionView::Template.registered_template_handler(:liquid)
|
||||||
|
template_details = {
|
||||||
|
format: Mime['html'].to_sym,
|
||||||
|
updated_at: email_template.updated_at,
|
||||||
|
virtual_path: 'test'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolver.find_templates('test', '', false, []).first.to_json
|
||||||
|
).to eq(
|
||||||
|
ActionView::Template.new(
|
||||||
|
email_template.body,
|
||||||
|
"DB Template - #{email_template.id}", handler, template_details
|
||||||
|
).to_json
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when account template exists in db' do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:installation_template) { create(:email_template, name: 'test', body: 'test') }
|
||||||
|
let(:account_template) { create(:email_template, name: 'test', body: 'test2', account: account) }
|
||||||
|
|
||||||
|
it 'return account template for current account' do
|
||||||
|
Current.account = account
|
||||||
|
handler = ActionView::Template.registered_template_handler(:liquid)
|
||||||
|
template_details = {
|
||||||
|
format: Mime['html'].to_sym,
|
||||||
|
updated_at: account_template.updated_at,
|
||||||
|
virtual_path: 'test'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolver.find_templates('test', '', false, []).first.to_json
|
||||||
|
).to eq(
|
||||||
|
ActionView::Template.new(
|
||||||
|
account_template.body,
|
||||||
|
"DB Template - #{account_template.id}", handler, template_details
|
||||||
|
).to_json
|
||||||
|
)
|
||||||
|
Current.account = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'return installation template when current account dont have template' do
|
||||||
|
Current.account = create(:account)
|
||||||
|
handler = ActionView::Template.registered_template_handler(:liquid)
|
||||||
|
template_details = {
|
||||||
|
format: Mime['html'].to_sym,
|
||||||
|
updated_at: installation_template.updated_at,
|
||||||
|
virtual_path: 'test'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolver.find_templates('test', '', false, []).first.to_json
|
||||||
|
).to eq(
|
||||||
|
ActionView::Template.new(
|
||||||
|
installation_template.body,
|
||||||
|
"DB Template - #{installation_template.id}", handler, template_details
|
||||||
|
).to_json
|
||||||
|
)
|
||||||
|
Current.account = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -31,6 +31,7 @@ RSpec.describe 'Confirmation Instructions', type: :mailer do
|
||||||
expect(mail.body).to match(
|
expect(mail.body).to match(
|
||||||
"#{CGI.escapeHTML(inviter_val.name)}, with #{CGI.escapeHTML(inviter_val.account.name)}, has invited you to try out Chatwoot!"
|
"#{CGI.escapeHTML(inviter_val.name)}, with #{CGI.escapeHTML(inviter_val.account.name)}, has invited you to try out Chatwoot!"
|
||||||
)
|
)
|
||||||
|
Current.account = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue