Skip to content

Commit a3927dc

Browse files
authored
[feature] Enable template variables in messages (#143)
Fixes #139
1 parent 88f4bca commit a3927dc

File tree

17 files changed

+235
-23
lines changed

17 files changed

+235
-23
lines changed

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ gem 'redis', '~> 4.0'
2828
# Use ActiveModel has_secure_password
2929
# gem 'bcrypt', '~> 3.1.7'
3030

31+
gem 'mustache', '~> 1.0'
32+
3133
# Use ActiveStorage variant
3234
# gem 'mini_magick', '~> 4.8'
3335

Gemfile.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ GEM
239239
multi_json (1.13.1)
240240
multi_xml (0.6.0)
241241
multipart-post (2.0.0)
242+
mustache (1.1.0)
242243
mysql2 (0.5.2)
243244
nenv (0.3.0)
244245
nio4r (2.3.1)
@@ -480,6 +481,7 @@ DEPENDENCIES
480481
jquery-ui-sass-rails
481482
listen (>= 3.0.5, < 3.2)
482483
minitest-reporters
484+
mustache (~> 1.0)
483485
mysql2 (>= 0.4.4, < 0.6.0)
484486
omniauth-mlh (~> 0.1)
485487
paperclip (~> 6.0)

app/controllers/manage/messages_controller.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ def deliver
5858
end
5959

6060
def preview
61-
email = Mailer.bulk_message_email(@message.id, current_user.id)
61+
email = Mailer.bulk_message_email(@message.id, current_user.id, nil, true)
6262
render html: email.body.raw_source.html_safe
6363
end
6464

6565
def live_preview
6666
body = params[:body] || ''
6767
message = Message.new(body: body)
68-
email = Mailer.bulk_message_email(nil, current_user.id, message)
68+
email = Mailer.bulk_message_email(nil, current_user.id, message, true)
6969
render html: email.body.raw_source.html_safe
7070
end
7171

app/mailers/mailer.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ class Mailer < ApplicationMailer
44

55
default from: -> { HackathonConfig['email_from'] }
66

7-
def bulk_message_email(message_id, user_id, message = nil)
8-
@message = message || Message.find_by_id(message_id)
9-
@user = User.find_by_id(user_id)
7+
def bulk_message_email(message_id, user_id, message = nil, use_examples = false)
8+
@message = message || Message.find_by_id(message_id)
9+
@user = User.find_by_id(user_id)
10+
@use_examples = use_examples
1011
return if @user.blank? || @message.blank?
1112
mail(
1213
to: pretty_email(@user.full_name, @user.email),

app/models/message.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ class Message < ApplicationRecord
44
validates_presence_of :name, :subject, :template
55
validates_presence_of :body, if: :using_default_template?
66

7+
validate :body_successfully_parses
8+
79
strip_attributes
810

911
POSSIBLE_TEMPLATES = ["default"].freeze
@@ -40,6 +42,29 @@ class Message < ApplicationRecord
4042
validates_inclusion_of :template, in: POSSIBLE_TEMPLATES
4143
validates_inclusion_of :type, in: POSSIBLE_TYPES
4244

45+
def parsed_body(context, use_examples = false)
46+
return body if body.blank?
47+
48+
message_template = MessageTemplate.new(context, use_examples)
49+
message_template.template = body
50+
message_template.render
51+
end
52+
53+
def valid_body?
54+
begin
55+
parsed_body(nil, true)
56+
rescue Mustache::Parser::SyntaxError
57+
return false
58+
end
59+
true
60+
end
61+
62+
def body_successfully_parses
63+
unless valid_body?
64+
errors.add(:body, 'failed to parse template variables')
65+
end
66+
end
67+
4368
def recipients=(values)
4469
values.present? ? super(values.reject(&:blank?)) : super(values)
4570
end

app/models/message_template.rb

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
class MessageTemplate < Mustache
2+
def initialize(context, use_examples = false)
3+
@var_context = context
4+
@use_examples = use_examples
5+
end
6+
7+
def method_missing(method_name, *arguments, &block)
8+
variable = MessageTemplate.variables[method_name.to_s]
9+
return evaluate(variable) if variable
10+
11+
super
12+
end
13+
14+
def respond_to_missing?(method_name, include_private = false)
15+
MessageTemplate.variables.key?(method_name.to_s) || super
16+
end
17+
18+
class << self
19+
def variables
20+
config["variables"]
21+
end
22+
23+
def config
24+
@config ||= reload_config
25+
end
26+
27+
def reload_config
28+
@config = YAML.load_file('config/template_variables.yml')
29+
end
30+
end
31+
32+
private
33+
34+
def evaluate(variable)
35+
return variable["example"] if @use_examples && !variable["use_value_as_example"]
36+
37+
value = variable["value"]
38+
instance_eval(value)
39+
end
40+
41+
def user
42+
@user ||= begin
43+
user_id = @var_context[:user_id]
44+
return nil if user_id.blank?
45+
46+
User.find_by_id(user_id)
47+
end
48+
end
49+
50+
def bus_list
51+
@bus_list ||= begin
52+
bus_list_id = @var_context[:bus_list_id] || user&.questionnaire&.bus_list_id
53+
return nil if bus_list_id.blank?
54+
55+
BusList.find_by_id(bus_list_id)
56+
end
57+
end
58+
end
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
1-
<%= markdown(@message.body) %>
1+
<%
2+
begin
3+
parsed_body = @message.parsed_body({ user_id: @user.id }, @use_examples)
4+
rescue Mustache::Parser::SyntaxError
5+
raise unless @use_examples
6+
parsed_body = @message.body
7+
end
8+
%>
9+
<%= markdown(parsed_body) %>

app/views/manage/messages/_form.html.haml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@
1919

2020
.form-actions.mb-3
2121
= f.button :submit, class: 'btn-primary'
22+
23+
= render 'templating'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
%h3.mt-5.pb-2#triggered-email-overview Templating
2+
3+
%p
4+
Message bodies can make use of template variables to help personalize and streamline emails.
5+
Templating is powered by <a target="_blank" href="https://mustache.github.io/mustache.5.html">mustache</a>.
6+
7+
%table.table.table-striped
8+
%thead
9+
%tr
10+
%th Variable
11+
%th About
12+
%th Example
13+
%tbody
14+
- example = MessageTemplate.new(nil, true)
15+
- MessageTemplate.variables.each do |variable, config|
16+
%tr
17+
%td
18+
%pre {{#{variable}}}
19+
%td
20+
%span= config["description"]
21+
%td
22+
%pre= example.send(variable)

config/template_variables.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
sections:
2+
user:
3+
description: The email recipient
4+
buslist:
5+
description: The bus list specified for the email, or the bus list a recipient is signed up for.
6+
variables:
7+
first_name:
8+
description: The recipient's first name, if it exists
9+
example: John
10+
value: user&.first_name
11+
last_name:
12+
description: The recipient's last name, if it exists
13+
example: Smith
14+
value: user&.last_name
15+
16+
bus_list_exists?:
17+
description: True/false if bus list information is available. Useful for optionally including bus content in emails.
18+
example: true
19+
value: bus_list.present?
20+
bus_list_name:
21+
description: The bus list's name.
22+
example: Toronto & Buffalo Bus
23+
value: bus_list&.name
24+
bus_list_notes:
25+
description: The bus list's bus notes
26+
example: |
27+
**Some notes for your bus:**
28+
29+
Picks up at 8:00am
30+
value: bus_list&.name
31+
32+
hackathon_name:
33+
description: Name of the hackathon
34+
use_value_as_example: true
35+
value: HackathonConfig['name']
36+
37+
apply_url:
38+
description: Full URL of the apply page. Useful for links.
39+
use_value_as_example: true
40+
value: Rails.application.routes.url_helpers.root_url(Rails.application.config.action_mailer.default_url_options)
41+
rsvp_url:
42+
description: Full URL of RSVP page. Useful for links.
43+
use_value_as_example: true
44+
value: Rails.application.routes.url_helpers.rsvp_url(Rails.application.config.action_mailer.default_url_options)
45+
rsvp_accept_url:
46+
description: Full URL to the RSVP acceptance page. Useful for a one-click RSVP link.
47+
use_value_as_example: true
48+
value: Rails.application.routes.url_helpers.accept_rsvp_url(Rails.application.config.action_mailer.default_url_options)
49+
rsvp_deny_url:
50+
description: Full URL to the RSVP denial page. Useful for a one-click RSVP link.
51+
use_value_as_example: true
52+
value: Rails.application.routes.url_helpers.deny_rsvp_url(Rails.application.config.action_mailer.default_url_options)

0 commit comments

Comments
 (0)