Class: EmailTemplate

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable, 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 :integer 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'
DEFAULT_TEMPLATE =
'email'
CATEGORIES =
%w[announcements events newsletters promotions transactional webinars reviews].freeze

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

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::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::EventPublishable

#publish_event

Instance Attribute Details

#bodyObject



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

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

#categoryObject (readonly)



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

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

#descriptionObject (readonly)



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

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

#stateObject (readonly)



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

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

#subjectObject



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

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

#system_codeObject (readonly)



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

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

#templateObject (readonly)



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

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

Class Method Details

.available_stylesheetsObject



136
137
138
139
140
# File 'app/models/email_template.rb', line 136

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

.available_templatesObject



142
143
144
145
146
147
# File 'app/models/email_template.rb', line 142

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.map { |f| File.basename(f).split('.')[0] }.compact.uniq.sort
end

.blank_template_idObject

Cached lookup for the BLANK template used for new communications



132
133
134
# File 'app/models/email_template.rb', line 132

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:



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

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:



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

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)



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

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



149
150
151
152
153
# File 'app/models/email_template.rb', line 149

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



111
112
113
114
115
116
117
118
119
# File 'app/models/email_template.rb', line 111

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



123
124
125
126
127
128
129
# File 'app/models/email_template.rb', line 123

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:



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

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

#activity_typesActiveRecord::Relation<ActivityType>

Returns:

See Also:



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

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

#activity_types_by_resultActiveRecord::Relation<ActivityType>

Returns:

See Also:



69
70
# File 'app/models/email_template.rb', line 69

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

#all_activity_type_referencedObject



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

def all_activity_type_referenced
  activity_types | activity_types_by_result
end

#allowed_categories(account) ⇒ Object



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

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



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

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

#belongs_to_resource=(val) ⇒ Object



174
175
176
177
178
179
180
# File 'app/models/email_template.rb', line 174

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



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

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

#campaign_emailsActiveRecord::Relation<CampaignEmail>

Returns:

See Also:



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

has_many :campaign_emails

#communicationsActiveRecord::Relation<Communication>

Returns:

See Also:



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

has_many :communications

#deep_dupObject



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'app/models/email_template.rb', line 95

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



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

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



218
219
220
221
222
223
224
# File 'app/models/email_template.rb', line 218

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



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

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)



245
246
247
248
249
250
251
# File 'app/models/email_template.rb', line 245

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:



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

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)


197
198
199
200
201
202
203
204
205
206
207
208
# File 'app/models/email_template.rb', line 197

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,
    /\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)


191
192
193
# File 'app/models/email_template.rb', line 191

def migrated_to_v4?
  body_v4.present?
end

#needs_v4_migration?Boolean

Check if this template has legacy content that needs migration

Returns:

  • (Boolean)


254
255
256
# File 'app/models/email_template.rb', line 254

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

#ok_to_delete?Boolean

Returns:

  • (Boolean)


155
156
157
# File 'app/models/email_template.rb', line 155

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)


212
213
214
# File 'app/models/email_template.rb', line 212

def r4_only?
  new_record? || !has_legacy_r3_content?
end

#referenced?Boolean

Returns:

  • (Boolean)


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

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



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

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



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

def render_body_v4(options = nil)
  return nil unless body_v4_email.present?

  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)



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

def render_editable_body(options = nil)
  return nil unless body_v4.present?

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

#render_from(options = nil) ⇒ Object



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

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

#render_subject(options = nil) ⇒ Object



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

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:



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

belongs_to :resource, polymorphic: true, optional: true

#to_sObject



317
318
319
# File 'app/models/email_template.rb', line 317

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