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

Modules: MergeFields Classes: ButtonPaddingNormalizer, 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 Models::Schedulable

Models::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 Models::Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Instance Attribute Details

#bodyObject

Validates subject, body, description, template, category.

Validations:



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

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

#categoryObject (readonly)

Validates subject, body, description, template, category.

Validations:



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

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

#descriptionObject (readonly)

Validates subject, body, description, template, category.

Validations:



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

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

#stateObject (readonly)

A system template may be active or archived, never draft. Code
resolves these by system_code, so a half-finished draft must not be
reachable — but archived is how a code-addressed template is retired
without deleting the row (and its communications history) outright.
Retiring one is loud, not silent: #assert_sendable! reports if the send
path ever resolves an archived template.

Validations:

  • Inclusion ({ in: %w[active archived], if: :system_code, message: 'System code presence requires an active or archived state' })


102
103
# File 'app/models/email_template.rb', line 102

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

#subjectObject

Validates subject, body, description, template, category.

Validations:



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

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

#system_codeObject (readonly)

Validates system code.

Validations:



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

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

#templateObject (readonly)

Validates subject, body, description, template, category.

Validations:



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

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

Class Method Details

.available_stylesheetsObject

Available stylesheets.



209
210
211
212
213
# File 'app/models/email_template.rb', line 209

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

Available templates.



216
217
218
219
220
221
# File 'app/models/email_template.rb', line 216

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



204
205
206
# File 'app/models/email_template.rb', line 204

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:



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

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:



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

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)

Parameters:

  • sender_party (Object)

    the sender party

  • theme (Object) (defaults to: :default)

    the theme



424
425
426
427
428
# File 'app/models/email_template.rb', line 424

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

Resource for select.



224
225
226
227
228
# File 'app/models/email_template.rb', line 224

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, include_id: nil) ⇒ Object

include_id: keeps a record's currently-assigned template selectable on
edit forms even when it is no longer active — otherwise the select renders
blank and a save silently drops the association.

Parameters:

  • conditions (Object) (defaults to: nil)

    the conditions

  • include_id (Integer) (defaults to: nil)

    the include id



183
184
185
186
187
188
189
190
191
# File 'app/models/email_template.rb', line 183

def self.select_options(conditions = nil, include_id: nil)
  res = conditions ? where(conditions) : active
  res = res.or(where(id: include_id)) if include_id
  res.order(:description).select(:id, :description, :subject, :state).map do |e|
    label = "#{e.description || e.subject} [#{e.id}]"
    label += " (#{e.state})" unless e.active?
    [label, e.id]
  end
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



195
196
197
198
199
200
201
# File 'app/models/email_template.rb', line 195

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 the associated activity chain types.

Returns:

  • (ActiveRecord::Relation<ActivityChainType>)

    the associated activity chain types



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

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

#activity_typesActiveRecord::Relation<ActivityType>

Returns the associated activity types.

Returns:

  • (ActiveRecord::Relation<ActivityType>)

    the associated activity types



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

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

#activity_types_by_resultActiveRecord::Relation<ActivityType>

Returns the associated activity types by result.

Returns:

  • (ActiveRecord::Relation<ActivityType>)

    the associated activity types by result



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

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

#all_activity_type_referencedObject

All activity type referenced.



250
251
252
# File 'app/models/email_template.rb', line 250

def all_activity_type_referenced
  activity_types | activity_types_by_result
end

#allowed_categories(account) ⇒ Object

Allowed categories.

Parameters:

  • account (Object)

    the account



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

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

#assert_sendable!void

This method returns an undefined value.

Reports — but does NOT block — when the send path resolves an archived
system template. Archiving is an inventory judgement made from send history,
and that history can be wrong: COLLECTIONS_30 and COLLECTIONS_DUE both still
send despite having no resolvable caller in app/ or lib/. If a template
was retired in error we want to hear about it, not drop a customer's email.



119
120
121
122
123
124
125
126
# File 'app/models/email_template.rb', line 119

def assert_sendable!
  return unless system_code.present? && archived?

  ErrorReporting.warning(
    "EmailTemplate #{system_code} is archived but was just resolved for sending — " \
    'either it was retired in error (reactivate it) or a caller needs removing.'
  )
end

#assortment_instructionsActiveRecord::Relation<AssortmentInstruction>

Returns the associated assortment instructions.

Returns:



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

has_many :assortment_instructions, inverse_of: :email_template

#belongs_to_resourceObject

Belongs to resource.



266
267
268
# File 'app/models/email_template.rb', line 266

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

#belongs_to_resource=(val) ⇒ Object

Sets the belongs to resource.

Parameters:

  • value (Object)

    the new belongs to resource

  • val (Object)

    the val



257
258
259
260
261
262
263
# File 'app/models/email_template.rb', line 257

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

Parameters:

  • val (Object)

    the val



344
345
346
347
# File 'app/models/email_template.rb', line 344

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

#campaign_emailsActiveRecord::Relation<CampaignEmail>

Returns the associated campaign emails.

Returns:

  • (ActiveRecord::Relation<CampaignEmail>)

    the associated campaign emails



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

has_many :campaign_emails

#campaignsActiveRecord::Relation<Campaign>

Returns the associated campaigns.

Returns:

  • (ActiveRecord::Relation<Campaign>)

    the associated campaigns



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

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

#communicationsActiveRecord::Relation<Communication>

Returns the associated communications.

Returns:

  • (ActiveRecord::Relation<Communication>)

    the associated communications



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

has_many :communications

#deep_dupObject

Returns a deep-copied, unsaved duplicate of the record.

Returns:

  • (Object)

    a deep-copied, unsaved duplicate of the record



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'app/models/email_template.rb', line 162

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



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

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



302
303
304
305
306
307
308
# File 'app/models/email_template.rb', line 302

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



318
319
320
321
322
323
324
# File 'app/models/email_template.rb', line 318

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)



329
330
331
332
333
334
335
# File 'app/models/email_template.rb', line 329

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

#embedded_assetsActiveRecord::Relation<EmbeddedAsset>

Returns the associated embedded assets.

Returns:

  • (ActiveRecord::Relation<EmbeddedAsset>)

    the associated embedded assets



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

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)


281
282
283
284
285
286
287
288
289
290
291
292
# File 'app/models/email_template.rb', line 281

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)


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

def migrated_to_v4?
  body_v4.present?
end

#needs_v4_migration?Boolean

Check if this template has legacy content that needs migration

Returns:

  • (Boolean)


338
339
340
# File 'app/models/email_template.rb', line 338

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

#ok_to_delete?Boolean

Returns whether the record ok to delete.

Returns:

  • (Boolean)

    whether the record ok to delete



231
232
233
# File 'app/models/email_template.rb', line 231

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)


296
297
298
# File 'app/models/email_template.rb', line 296

def r4_only?
  new_record? || !has_legacy_r3_content?
end

#referenced?Boolean

Returns whether the record referenced.

Returns:

  • (Boolean)

    whether the record referenced



236
237
238
# File 'app/models/email_template.rb', line 236

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

#render_body(options = nil) ⇒ String

Renders body as Liquid Markup template

Parameters:

  • options (Hash, nil) (defaults to: nil)

    Liquid assigns (merge variables) for the template

Options Hash (options):

  • ignore_errors (Boolean)

    suppress the error report when the template renders with errors

Returns:

  • (String)

    the rendered body



358
359
360
# File 'app/models/email_template.rb', line 358

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

#render_body_v4(options = nil) ⇒ String?

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

Parameters:

  • options (Hash, nil) (defaults to: nil)

    Liquid assigns (merge variables) for the template

Options Hash (options):

  • ignore_errors (Boolean)

    suppress the error report when the template renders with errors

Returns:

  • (String, nil)

    the rendered body



368
369
370
371
372
373
# File 'app/models/email_template.rb', line 368

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) ⇒ String?

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

Parameters:

  • options (Hash, nil) (defaults to: nil)

    Liquid assigns (merge variables) for the template

Options Hash (options):

  • ignore_errors (Boolean)

    suppress the error report when the template renders with errors

Returns:

  • (String, nil)

    the rendered body



381
382
383
384
385
386
# File 'app/models/email_template.rb', line 381

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) ⇒ String

Renders the sender address as a Liquid template.

Parameters:

  • options (Hash, nil) (defaults to: nil)

    Liquid assigns (merge variables) for the template

Options Hash (options):

  • ignore_errors (Boolean)

    suppress the error report when the template renders with errors

Returns:

  • (String)

    the rendered sender address



402
403
404
# File 'app/models/email_template.rb', line 402

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

#render_subject(options = nil) ⇒ String

Renders the subject as a Liquid template.

Parameters:

  • options (Hash, nil) (defaults to: nil)

    Liquid assigns (merge variables) for the template

Options Hash (options):

  • ignore_errors (Boolean)

    suppress the error report when the template renders with errors

Returns:

  • (String)

    the rendered subject



393
394
395
# File 'app/models/email_template.rb', line 393

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_sString

Returns string representation of the record.

Returns:

  • (String)

    string representation of the record



431
432
433
# File 'app/models/email_template.rb', line 431

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