mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +00:00 
			
		
		
		
	feat: Customisable Email Templates (#1095)
This commit is contained in:
		
							
								
								
									
										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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Sojan Jose
					Sojan Jose