Class: EmailTemplate

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable, Models::EventPublishable, PgSearch::Model
Defined in:
app/models/email_template.rb

Overview

== Schema Information

Table name: email_templates
Database name: primary

id :integer not null, primary key
bcc :string(255)
body :text
body_v4 :text
body_v4_email :text
category :string
cc :string(255)
css :text
css_v4 :text
default_from :string
default_reply_to :string
description :string(255)
disable_premailer :boolean default(FALSE), not null
disable_rich_editing :boolean default(FALSE), not null
from :string(255)
group :string
preview_text :string
redactor_4_ready :boolean default(FALSE), not null
resource_type :string(255)
state :enum default("active")
stylesheet :string(255)
subject :string(255)
system_code :string(20)
template :string
to :string(255)
uses_redactor_v4 :boolean
created_at :datetime not null
updated_at :datetime not null
creator_id :integer
resource_id :integer
updater_id :integer

Indexes

by_state_resource_is_null (state) WHERE (resource_id IS NULL)
email_templates_group_idx (group)
index_email_templates_on_resource_type_and_resource_id (resource_type,resource_id)
index_email_templates_on_state (state)
index_email_templates_on_system_code (system_code) UNIQUE

Defined Under Namespace

Classes: ContentMigrator

Constant Summary collapse

DEFAULT_STYLESHEET =

Default stylesheet.

'default'
DEFAULT_TEMPLATE =

Default template.

'email'
CATEGORIES =

Categories.

%w[announcements events newsletters promotions transactional webinars reviews].freeze

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::EventPublishable

#publish_event

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Instance Attribute Details

#bodyObject



83
# File 'app/models/email_template.rb', line 83

validates :subject, :body, :description, :template, :category, presence: true

#categoryObject (readonly)



83
# File 'app/models/email_template.rb', line 83

validates :subject, :body, :description, :template, :category, presence: true

#descriptionObject (readonly)



83
# File 'app/models/email_template.rb', line 83

validates :subject, :body, :description, :template, :category, presence: true

#stateObject (readonly)



85
86
# File 'app/models/email_template.rb', line 85

validates :state, inclusion: { in: %w[active], if: :system_code,
message: 'System code presence requires an active state' }

#subjectObject



83
# File 'app/models/email_template.rb', line 83

validates :subject, :body, :description, :template, :category, presence: true

#system_codeObject (readonly)



84
# File 'app/models/email_template.rb', line 84

validates :system_code, length: { maximum: 20 }, uniqueness: true, allow_nil: true

#templateObject (readonly)



83
# File 'app/models/email_template.rb', line 83

validates :subject, :body, :description, :template, :category, presence: true

Class Method Details

.available_stylesheetsObject



152
153
154
155
156
# File 'app/models/email_template.rb', line 152

def self.available_stylesheets
  stylesheets_dir = Rails.public_path.join('stylesheets/emails/*.css')
  files = Dir.glob(stylesheets_dir)
  files.filter_map { |f| File.basename(f).split('.')[0] }.uniq.sort
end

.available_templatesObject



158
159
160
161
162
163
# File 'app/models/email_template.rb', line 158

def self.available_templates
  template_dir = Rails.root.join('app/views/communication_mailer/*.erb')
  files = Dir.glob(template_dir)
  files.delete_if { |x| x.include?('/_') } # remove partials
  files.filter_map { |f| File.basename(f).split('.')[0] }.uniq.sort
end

.blank_template_idObject

Cached lookup for the BLANK template used for new communications



148
149
150
# File 'app/models/email_template.rb', line 148

def self.blank_template_id
  @blank_template_id ||= find_by(system_code: 'BLANK')&.id
end

.non_campaignActiveRecord::Relation<EmailTemplate>

A relation of EmailTemplates that are non campaign. Active Record Scope

Returns:

See Also:



108
# File 'app/models/email_template.rb', line 108

scope :non_campaign, -> { where("description not like 'Template for campaign email%'") }

.non_systemActiveRecord::Relation<EmailTemplate>

A relation of EmailTemplates that are non system. Active Record Scope

Returns:

See Also:



109
# File 'app/models/email_template.rb', line 109

scope :non_system, -> { where(system_code: [nil, '']) }

.render_signature(sender_party, theme: :default) ⇒ Object

Renders a signature as text using the signature partial we have already
theme: :default (creamy) or :technical (grey-blue for support case templates)



327
328
329
330
331
# File 'app/models/email_template.rb', line 327

def self.render_signature(sender_party, theme: :default)
  # av = ActionView::Base.new(Heatwave::Application.config.paths['app/views'].first)
  ApplicationController.render(partial: 'communications/signature', layout: false,
                               locals: { sender_party:, theme: })
end

.resource_for_selectObject



165
166
167
168
169
# File 'app/models/email_template.rb', line 165

def self.resource_for_select
  [] +
    Employee.select_options.map { |e| [e[0], "Employee|#{e[1]}"] } +
    Company.select_options_sales_companies.map { |e| [e[0], "Company|#{e[1]}"] }
end

.select_options(conditions = nil) ⇒ Object



127
128
129
130
131
132
133
134
135
# File 'app/models/email_template.rb', line 127

def self.select_options(conditions = nil)
  res = order(:description).select(:id, :description, :subject)
  res = if conditions
          res.where(conditions)
        else
          res.active
        end
  res.map { |e| ["#{e.description || e.subject} [#{e.id}]", e.id] }
end

.select_options_r4_campaignObject

Select options for campaign email cloning - only Redactor 4 ready templates
Returns array of [label, id] with Redactor 4 badge indicator



139
140
141
142
143
144
145
# File 'app/models/email_template.rb', line 139

def self.select_options_r4_campaign
  order(:description)
    .select(:id, :description, :subject)
    .where(group: 'campaign', redactor_4_ready: true)
    .active
    .map { |e| ["#{e.description || e.subject} [#{e.id}] 🟢 Redactor 4", e.id] }
end

Instance Method Details

#activity_chain_typesActiveRecord::Relation<ActivityChainType>

Returns:

See Also:



75
# File 'app/models/email_template.rb', line 75

has_many :activity_chain_types, dependent: :nullify, inverse_of: :email_template

#activity_typesActiveRecord::Relation<ActivityType>

Returns:

See Also:



74
# File 'app/models/email_template.rb', line 74

has_many :activity_types, dependent: :nullify, inverse_of: :email_template

#activity_types_by_resultActiveRecord::Relation<ActivityType>

Returns:

See Also:



76
77
# File 'app/models/email_template.rb', line 76

has_many :activity_types_by_result, class_name: 'ActivityType',
through: :activity_chain_types, source: :activity_type

#all_activity_type_referencedObject



186
187
188
# File 'app/models/email_template.rb', line 186

def all_activity_type_referenced
  activity_types | activity_types_by_result
end

#allowed_categories(account) ⇒ Object



179
180
181
182
183
184
# File 'app/models/email_template.rb', line 179

def allowed_categories()
  categories = EmailTemplate::CATEGORIES.dup
  categories.delete('transactional') unless .has_role?('marketing_rep')
  categories << category
  categories.compact.uniq.sort
end

#belongs_to_resourceObject



198
199
200
# File 'app/models/email_template.rb', line 198

def belongs_to_resource
  resource ? "#{resource.class.name}|#{resource_id}" : nil
end

#belongs_to_resource=(val) ⇒ Object



190
191
192
193
194
195
196
# File 'app/models/email_template.rb', line 190

def belongs_to_resource=(val)
  if val.present? && !val.index('|').nil?
    self.resource_type, self.resource_id = val.split('|')
  else
    self.resource = nil
  end
end

#body_v4=(val) ⇒ Object

Setter for body_v4 that also clears the template cache



275
276
277
278
# File 'app/models/email_template.rb', line 275

def body_v4=(val)
  self[:body_v4] = val
  @body_v4_template = nil
end

#campaign_emailsActiveRecord::Relation<CampaignEmail>

Returns:

See Also:



78
# File 'app/models/email_template.rb', line 78

has_many :campaign_emails

#campaignsActiveRecord::Relation<Campaign>

Returns:

See Also:



79
# File 'app/models/email_template.rb', line 79

has_many :campaigns, -> { distinct }, through: :campaign_emails

#communicationsActiveRecord::Relation<Communication>

Returns:

See Also:



80
# File 'app/models/email_template.rb', line 80

has_many :communications

#deep_dupObject



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'app/models/email_template.rb', line 111

def deep_dup
  deep_clone(except: %i[system_code resource_id resource_type category]) do |original, copy|
    next unless copy.is_a?(EmailTemplate)

    copy.description = "Copy of #{original.description}"
    if original.redactor_4_ready?
      copy.redactor_4_ready = true
      copy.body_v4 = original.body_v4 if original.body_v4.present?
      copy.body_v4_email = original.body_v4_email if original.body_v4_email.present?
      copy.css_v4 = original.css_v4 if original.css_v4.present?
      copy.body = '<p>This template uses Redactor 4. Edit the content in the editor below.</p>'
      copy.css = nil
    end
  end
end

#editable_bodyObject

Returns the body content for editing in Redactor
Uses semantic v4 content, not the email-ready version



244
245
246
# File 'app/models/email_template.rb', line 244

def editable_body
  body_v4.presence || body
end

#effective_bodyObject

Returns the body content to use for rendering/sending emails
When redactor_4_ready is true, uses v4 content; otherwise uses legacy body + css



234
235
236
237
238
239
240
# File 'app/models/email_template.rb', line 234

def effective_body
  if redactor_4_ready?
    body_v4_email.presence
  else
    body
  end
end

#effective_cssObject

Returns the CSS to use for rendering
When redactor_4_ready is true, uses v4 CSS (usually empty); otherwise uses legacy CSS



250
251
252
253
254
255
256
# File 'app/models/email_template.rb', line 250

def effective_css
  if redactor_4_ready?
    ''
  else
    css
  end
end

#effective_templateObject

Returns the template to use for sending emails
When redactor_4_ready is true, uses 'v4' template (raw output, body_v4_email is already complete HTML)
Otherwise uses the configured template (or default)



261
262
263
264
265
266
267
# File 'app/models/email_template.rb', line 261

def effective_template
  if redactor_4_ready? && body_v4_email.present?
    'v4'
  else
    template.presence || DEFAULT_TEMPLATE
  end
end

#embedded_assetsActiveRecord::Relation<EmbeddedAsset>

Returns:

See Also:



81
# File 'app/models/email_template.rb', line 81

has_many :embedded_assets, as: :parent, dependent: :destroy

#has_legacy_r3_content?Boolean

Check if this template has meaningful Redactor 3 content
Returns false for new records, blank body, or placeholder content

Returns:

  • (Boolean)


213
214
215
216
217
218
219
220
221
222
223
224
# File 'app/models/email_template.rb', line 213

def has_legacy_r3_content?
  return false if new_record?
  return false if body.blank?

  # Check if body is just a placeholder (from copy or new template)
  placeholder_patterns = [
    /\A\s*<p>\s*This template uses Redactor 4/i,
    %r{\A\s*<p>\s*</p>\s*\z},
    /\A\s*\z/
  ]
  placeholder_patterns.none? { |pattern| body.match?(pattern) }
end

#migrated_to_v4?Boolean

Check if this template has been migrated to Redactor 4

Returns:

  • (Boolean)


207
208
209
# File 'app/models/email_template.rb', line 207

def migrated_to_v4?
  body_v4.present?
end

#needs_v4_migration?Boolean

Check if this template has legacy content that needs migration

Returns:

  • (Boolean)


270
271
272
# File 'app/models/email_template.rb', line 270

def needs_v4_migration?
  body.present? && body_v4.blank?
end

#ok_to_delete?Boolean

Returns:

  • (Boolean)


171
172
173
# File 'app/models/email_template.rb', line 171

def ok_to_delete?
  !referenced? and system_code.blank?
end

#r4_only?Boolean

Check if this template should be R4-only (no toggle, no R3 panels)
True for new templates or templates without meaningful R3 content

Returns:

  • (Boolean)


228
229
230
# File 'app/models/email_template.rb', line 228

def r4_only?
  new_record? || !has_legacy_r3_content?
end

#referenced?Boolean

Returns:

  • (Boolean)


175
176
177
# File 'app/models/email_template.rb', line 175

def referenced?
  activity_types.any? or activity_chain_types.any? or campaign_emails.any?
end

#render_body(options = nil) ⇒ Object

Renders body as Liquid Markup template



285
286
287
# File 'app/models/email_template.rb', line 285

def render_body(options = nil)
  Liquid::Renderer.render(body_template_instance, options, to_s)
end

#render_body_v4(options = nil) ⇒ Object

Renders body_v4_email content directly, bypassing redactor_4_ready? check
Used for admin previews when body_v4 exists but template is not yet marked ready



291
292
293
294
295
296
# File 'app/models/email_template.rb', line 291

def render_body_v4(options = nil)
  return nil if body_v4_email.blank?

  template = Liquid::ParseEnvironment.parse(body_v4_email.to_s)
  Liquid::Renderer.render(template, options, to_s)
end

#render_editable_body(options = nil) ⇒ Object

Renders body_v4 content for loading into Redactor 4 editor
Uses body_v4 (editor content), not body_v4_email (email output)



300
301
302
303
304
305
# File 'app/models/email_template.rb', line 300

def render_editable_body(options = nil)
  return nil if body_v4.blank?

  template = Liquid::ParseEnvironment.parse(body_v4.to_s)
  Liquid::Renderer.render(template, options, to_s)
end

#render_from(options = nil) ⇒ Object



311
312
313
# File 'app/models/email_template.rb', line 311

def render_from(options = nil)
  Liquid::Renderer.render(sender_email_template_instance, options, to_s)
end

#render_subject(options = nil) ⇒ Object



307
308
309
# File 'app/models/email_template.rb', line 307

def render_subject(options = nil)
  Liquid::Renderer.render(subject_template_instance, options, to_s)
end

#resourceResource

To represent ownership, thinking Employee, Company or nil

Returns:

  • (Resource)

See Also:



72
# File 'app/models/email_template.rb', line 72

belongs_to :resource, polymorphic: true, optional: true

#to_sObject



333
334
335
# File 'app/models/email_template.rb', line 333

def to_s
  "[EmailTemplate:#{id}] #{description}"
end