Class: EmailTemplate
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- EmailTemplate
- 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
-
#body ⇒ Object
Validates subject, body, description, template, category.
-
#category ⇒ Object
readonly
Validates subject, body, description, template, category.
-
#description ⇒ Object
readonly
Validates subject, body, description, template, category.
-
#state ⇒ Object
readonly
A system template may be
activeorarchived, neverdraft. -
#subject ⇒ Object
Validates subject, body, description, template, category.
-
#system_code ⇒ Object
readonly
Validates system code.
-
#template ⇒ Object
readonly
Validates subject, body, description, template, category.
Belongs to collapse
-
#resource ⇒ Resource
To represent ownership, thinking Employee, Company or nil.
Methods included from Models::Auditable
Has many collapse
-
#activity_chain_types ⇒ ActiveRecord::Relation<ActivityChainType>
The associated activity chain types.
-
#activity_types ⇒ ActiveRecord::Relation<ActivityType>
The associated activity types.
-
#activity_types_by_result ⇒ ActiveRecord::Relation<ActivityType>
The associated activity types by result.
-
#assortment_instructions ⇒ ActiveRecord::Relation<AssortmentInstruction>
The associated assortment instructions.
-
#campaign_emails ⇒ ActiveRecord::Relation<CampaignEmail>
The associated campaign emails.
-
#campaigns ⇒ ActiveRecord::Relation<Campaign>
The associated campaigns.
-
#communications ⇒ ActiveRecord::Relation<Communication>
The associated communications.
-
#embedded_assets ⇒ ActiveRecord::Relation<EmbeddedAsset>
The associated embedded assets.
Class Method Summary collapse
-
.available_stylesheets ⇒ Object
Available stylesheets.
-
.available_templates ⇒ Object
Available templates.
-
.blank_template_id ⇒ Object
Cached lookup for the BLANK template used for new communications.
-
.non_campaign ⇒ ActiveRecord::Relation<EmailTemplate>
A relation of EmailTemplates that are non campaign.
-
.non_system ⇒ ActiveRecord::Relation<EmailTemplate>
A relation of EmailTemplates that are non system.
-
.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).
-
.resource_for_select ⇒ Object
Resource for select.
-
.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. -
.select_options_r4_campaign ⇒ Object
Select options for campaign email cloning - only Redactor 4 ready templates Returns array of [label, id] with Redactor 4 badge indicator.
Instance Method Summary collapse
-
#all_activity_type_referenced ⇒ Object
All activity type referenced.
-
#allowed_categories(account) ⇒ Object
Allowed categories.
-
#assert_sendable! ⇒ void
Reports — but does NOT block — when the send path resolves an archived system template.
-
#belongs_to_resource ⇒ Object
Belongs to resource.
-
#belongs_to_resource=(val) ⇒ Object
Sets the belongs to resource.
-
#body_v4=(val) ⇒ Object
Setter for body_v4 that also clears the template cache.
-
#deep_dup ⇒ Object
A deep-copied, unsaved duplicate of the record.
-
#editable_body ⇒ Object
Returns the body content for editing in Redactor Uses semantic v4 content, not the email-ready version.
-
#effective_body ⇒ Object
Returns the body content to use for rendering/sending emails When redactor_4_ready is true, uses v4 content; otherwise uses legacy body + css.
-
#effective_css ⇒ Object
Returns the CSS to use for rendering When redactor_4_ready is true, uses v4 CSS (usually empty); otherwise uses legacy CSS.
-
#effective_template ⇒ Object
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).
-
#has_legacy_r3_content? ⇒ Boolean
Check if this template has meaningful Redactor 3 content Returns false for new records, blank body, or placeholder content.
-
#migrated_to_v4? ⇒ Boolean
Check if this template has been migrated to Redactor 4.
-
#needs_v4_migration? ⇒ Boolean
Check if this template has legacy content that needs migration.
-
#ok_to_delete? ⇒ Boolean
Whether the record ok to delete.
-
#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.
-
#referenced? ⇒ Boolean
Whether the record referenced.
-
#render_body(options = nil) ⇒ String
Renders body as Liquid Markup template.
-
#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.
-
#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).
-
#render_from(options = nil) ⇒ String
Renders the sender address as a Liquid template.
-
#render_subject(options = nil) ⇒ String
Renders the subject as a Liquid template.
-
#to_s ⇒ String
String representation of the record.
Methods included from Models::EventPublishable
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
Methods included from Models::AfterCommittable
Instance Attribute Details
#body ⇒ Object
Validates subject, body, description, template, category.
Validations:
93 |
# File 'app/models/email_template.rb', line 93 validates :subject, :body, :description, :template, :category, presence: true |
#category ⇒ Object (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 |
#description ⇒ Object (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 |
#state ⇒ Object (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' } |
#subject ⇒ Object
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_code ⇒ Object (readonly)
Validates system code.
Validations:
- Length ({ maximum: 20 })
- Uniqueness
- Allow_nil
95 |
# File 'app/models/email_template.rb', line 95 validates :system_code, length: { maximum: 20 }, uniqueness: true, allow_nil: true |
#template ⇒ Object (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_stylesheets ⇒ Object
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_templates ⇒ Object
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_id ⇒ Object
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_campaign ⇒ ActiveRecord::Relation<EmailTemplate>
A relation of EmailTemplates that are non campaign. Active Record Scope
158 |
# File 'app/models/email_template.rb', line 158 scope :non_campaign, -> { where("description not like 'Template for campaign email%'") } |
.non_system ⇒ ActiveRecord::Relation<EmailTemplate>
A relation of EmailTemplates that are non system. Active Record Scope
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)
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_select ⇒ Object
Resource for select.
224 225 226 227 228 |
# File 'app/models/email_template.rb', line 224 def self.resource_for_select [] + Employee..map { |e| [e[0], "Employee|#{e[1]}"] } + Company..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.
183 184 185 186 187 188 189 190 191 |
# File 'app/models/email_template.rb', line 183 def self.(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_campaign ⇒ Object
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. 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_types ⇒ ActiveRecord::Relation<ActivityChainType>
Returns 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_types ⇒ ActiveRecord::Relation<ActivityType>
Returns 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_result ⇒ ActiveRecord::Relation<ActivityType>
Returns 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_referenced ⇒ Object
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.
242 243 244 245 246 247 |
# File 'app/models/email_template.rb', line 242 def allowed_categories(account) categories = EmailTemplate::CATEGORIES.dup categories.delete('transactional') unless account.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_instructions ⇒ ActiveRecord::Relation<AssortmentInstruction>
Returns the associated assortment instructions.
90 |
# File 'app/models/email_template.rb', line 90 has_many :assortment_instructions, inverse_of: :email_template |
#belongs_to_resource ⇒ Object
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.
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
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_emails ⇒ ActiveRecord::Relation<CampaignEmail>
Returns the associated campaign emails.
82 |
# File 'app/models/email_template.rb', line 82 has_many :campaign_emails |
#campaigns ⇒ ActiveRecord::Relation<Campaign>
Returns the associated campaigns.
84 |
# File 'app/models/email_template.rb', line 84 has_many :campaigns, -> { distinct }, through: :campaign_emails |
#communications ⇒ ActiveRecord::Relation<Communication>
Returns the associated communications.
86 |
# File 'app/models/email_template.rb', line 86 has_many :communications |
#deep_dup ⇒ Object
Returns 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_body ⇒ Object
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_body ⇒ Object
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_css ⇒ Object
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_template ⇒ Object
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_assets ⇒ ActiveRecord::Relation<EmbeddedAsset>
Returns 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
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
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
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.
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
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.
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
358 359 360 |
# File 'app/models/email_template.rb', line 358 def render_body( = nil) Liquid::Renderer.render(body_template_instance, , 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
368 369 370 371 372 373 |
# File 'app/models/email_template.rb', line 368 def render_body_v4( = nil) return nil if body_v4_email.blank? template = Liquid::ParseEnvironment.parse(body_v4_email.to_s) Liquid::Renderer.render(template, , 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)
381 382 383 384 385 386 |
# File 'app/models/email_template.rb', line 381 def render_editable_body( = nil) return nil if body_v4.blank? template = Liquid::ParseEnvironment.parse(body_v4.to_s) Liquid::Renderer.render(template, , to_s) end |
#render_from(options = nil) ⇒ String
Renders the sender address as a Liquid template.
402 403 404 |
# File 'app/models/email_template.rb', line 402 def render_from( = nil) Liquid::Renderer.render(sender_email_template_instance, , to_s) end |
#render_subject(options = nil) ⇒ String
Renders the subject as a Liquid template.
393 394 395 |
# File 'app/models/email_template.rb', line 393 def render_subject( = nil) Liquid::Renderer.render(subject_template_instance, , to_s) end |
#resource ⇒ Resource
To represent ownership, thinking Employee, Company or nil
72 |
# File 'app/models/email_template.rb', line 72 belongs_to :resource, polymorphic: true, optional: true |
#to_s ⇒ String
Returns string representation of the record.
431 432 433 |
# File 'app/models/email_template.rb', line 431 def to_s "[EmailTemplate:#{id}] #{description}" end |