effective_events 0.1.10 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '069d27a813f8daeb16748df4948a0e653dbe0bb8d3a853e5fe8ef24fda74729c'
4
- data.tar.gz: 2214b6dbdac9cc9e824948303ff983a4aef264fc4544bb43dca0d208a492612c
3
+ metadata.gz: cd744d00aed47ee8fe88bbfa4f9224f6dea924726a9d7f42be397e88315e5a0f
4
+ data.tar.gz: b24c00d8bc3a54e3ccad2f43db728ede8a53b5aaff618646386c3ad179c827ed
5
5
  SHA512:
6
- metadata.gz: 37f1f2d5c00228a0bfbc67ca1aeb49eb70b0691945c06bea4a2432d307658059bbc14bae09d7089c9bea649ab45ecd849bb44af087a90792f3d30827c45411bc
7
- data.tar.gz: 41f9da86b0be58b72eff25b4fd1853105e63be29782787135d90abd8e38fcec7910da9846e640f845635a7424039c3d852e0ebe4fb0032fbaadbd369bc1c4931
6
+ metadata.gz: bee221260b8e2aad273c9b79250707bf4c053dc64a937c3eff477eb345e8bc3a10afb4b9504d45f2d2be778a842a62ffc7dc4a9ac76138915575eff8c295524c
7
+ data.tar.gz: 305c28be0450a00d070e15d32d0af3dc2de4c72bdbad1e345948c00851ccb882566b84b9a256d9c62786573902cced56a63a658204fd671614d4e59ebc3a58f2
@@ -0,0 +1,8 @@
1
+ module Admin
2
+ class EventNotificationsController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_events) }
5
+
6
+ include Effective::CrudController
7
+ end
8
+ end
@@ -39,7 +39,6 @@ module Effective
39
39
 
40
40
  def permitted_params
41
41
  model = (params.key?(:effective_event_registration) ? :effective_event_registration : :event_registration)
42
-
43
42
  params.require(model).permit!.except(:status, :status_steps, :wizard_steps, :submitted_at)
44
43
  end
45
44
 
@@ -8,7 +8,7 @@ module Effective
8
8
  }
9
9
 
10
10
  def index
11
- @events ||= resource_scope
11
+ @events ||= resource_scope.published
12
12
 
13
13
  @events = @events.paginate(page: params[:page])
14
14
 
@@ -0,0 +1,52 @@
1
+ class Admin::EffectiveEventNotificationsDatatable < Effective::Datatable
2
+ # filters do
3
+ # scope :all
4
+ # scope :started
5
+ # scope :completed
6
+ # end
7
+
8
+ datatable do
9
+ col :updated_at, visible: false
10
+ col :created_at, visible: false
11
+ col :id, visible: false
12
+
13
+ col :event
14
+ col :category
15
+
16
+ col :from do |event|
17
+ html_escape(event.from)
18
+ end
19
+
20
+ # col :reminder do |poll_notification|
21
+ # case poll_notification.category
22
+ # when 'When poll starts'
23
+ # poll_notification.poll.start_at&.strftime('%F %H:%M')
24
+ # when 'When poll ends'
25
+ # poll_notification.poll.end_at&.strftime('%F %H:%M')
26
+ # when 'Upcoming reminder'
27
+ # Effective::PollNotification::UPCOMING_REMINDERS.invert[poll_notification.reminder]
28
+ # when 'Reminder'
29
+ # Effective::PollNotification::REMINDERS.invert[poll_notification.reminder]
30
+ # when 'Before poll ends'
31
+ # Effective::PollNotification::UPCOMING_REMINDERS.invert[poll_notification.reminder]
32
+ # else
33
+ # raise('unexpected category')
34
+ # end
35
+ # end
36
+
37
+ col :subject
38
+
39
+ col :body do |notification|
40
+ simple_format(notification.body)
41
+ end
42
+
43
+ # col :started_at, visible: false
44
+ # col :completed_at
45
+
46
+ actions_col
47
+ end
48
+
49
+ collection do
50
+ Effective::EventNotification.all.deep
51
+ end
52
+ end
@@ -14,6 +14,8 @@ module Admin
14
14
  col :id, visible: false
15
15
 
16
16
  col :title
17
+ col :slug, visible: false
18
+
17
19
  col :draft
18
20
  col :start_at, label: 'Event Start Date'
19
21
  col :end_at, label: 'Event End Date', visible: false
@@ -0,0 +1,56 @@
1
+ module Effective
2
+ class EventsMailer < EffectiveResources.parent_mailer_class
3
+ default from: -> { EffectiveResources.mailer_sender }
4
+ layout -> { EffectiveResources.mailer_layout }
5
+
6
+ include EffectiveEmailTemplatesMailer if EffectiveEvents.use_effective_email_templates
7
+
8
+ def event_registrant_purchased(event_registrant, opts = {})
9
+ @assigns = assigns_for(event_registrant)
10
+ @event_registrant = event_registrant
11
+
12
+ mail(to: event_registrant.email, **headers_for(event_registrant, opts))
13
+ end
14
+
15
+ protected
16
+
17
+ def headers_for(resource, opts = {})
18
+ resource.respond_to?(:log_changes_datatable) ? opts.merge(log: resource) : opts
19
+ end
20
+
21
+ def assigns_for(resource)
22
+ if resource.kind_of?(EventRegistrant)
23
+ return event_registrant_assigns(resource).merge(event_assigns(resource.event)).merge(event_ticket_assigns(resource.event_ticket))
24
+ end
25
+
26
+ raise('unexpected resource')
27
+ end
28
+
29
+ def event_assigns(resource)
30
+ raise('expected an event') unless resource.kind_of?(Event)
31
+
32
+ values = {
33
+ name: resource.title,
34
+ date: resource.start_at.strftime('%F %H:%M'),
35
+ url: effective_events.event_url(resource)
36
+ }
37
+
38
+ { event: values }
39
+ end
40
+
41
+ def event_ticket_assigns(resource)
42
+ raise('expected an event ticket') unless resource.kind_of?(EventTicket)
43
+
44
+ values = { name: resource.title }
45
+ { ticket: values }
46
+ end
47
+
48
+ def event_registrant_assigns(resource)
49
+ raise('expected an event registrant') unless resource.kind_of?(EventRegistrant)
50
+
51
+ values = { name: resource.name, email: resource.email }
52
+ { registrant: values }
53
+ end
54
+
55
+ end
56
+ end
@@ -118,6 +118,11 @@ module EffectiveEventsEventRegistration
118
118
  (event_registrants + event_addons)
119
119
  end
120
120
 
121
+ def after_submit_purchased!
122
+ notifications = event.event_notifications.select(&:registrant_purchased?)
123
+ notifications.each { |notification| notification.notify!(event_registrants: event_registrants) }
124
+ end
125
+
121
126
  end
122
127
 
123
128
  # Instance Methods
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Effective
4
4
  class Event < ActiveRecord::Base
5
- has_many :event_tickets, -> { EventTicket.sorted }, inverse_of: :event
5
+ has_many :event_tickets, -> { EventTicket.sorted }, inverse_of: :event, dependent: :destroy
6
6
  accepts_nested_attributes_for :event_tickets, allow_destroy: true
7
7
 
8
- has_many :event_products, -> { EventProduct.sorted }, inverse_of: :event
8
+ has_many :event_products, -> { EventProduct.sorted }, inverse_of: :event, dependent: :destroy
9
9
  accepts_nested_attributes_for :event_products, allow_destroy: true
10
10
 
11
11
  has_many :event_registrants, -> { order(:event_ticket_id, :created_at) }, inverse_of: :event
@@ -14,6 +14,9 @@ module Effective
14
14
  has_many :event_addons, -> { order(:event_product_id, :created_at) }, inverse_of: :event
15
15
  accepts_nested_attributes_for :event_addons, allow_destroy: true
16
16
 
17
+ has_many :event_notifications, -> { order(:id) }, inverse_of: :event, dependent: :destroy
18
+ accepts_nested_attributes_for :event_notifications, allow_destroy: true
19
+
17
20
  # rich_text_body - Used by the select step
18
21
  has_many_rich_texts
19
22
 
@@ -0,0 +1,160 @@
1
+ module Effective
2
+ class EventNotification < ActiveRecord::Base
3
+ log_changes(to: :event) if respond_to?(:log_changes)
4
+
5
+ belongs_to :event
6
+
7
+ CATEGORIES = ['Registrant purchased']
8
+ EMAIL_TEMPLATE_VARIABLES = ['event.name', 'event.date', 'event.url', 'ticket.name', 'registrant.name', 'registrant.email']
9
+
10
+ #CATEGORIES = ['Upcoming reminder', 'When event starts', 'Reminder', 'Before event ends', 'When event ends']
11
+
12
+ # UPCOMING_REMINDERS = {
13
+ # '1 hour before' => 1.hours.to_i,
14
+ # '3 hours before' => 3.hours.to_i,
15
+ # '6 hours before' => 6.hours.to_i,
16
+ # '12 hours before' => 12.hours.to_i,
17
+ # '1 day before' => 1.days.to_i,
18
+ # '2 days before' => 2.days.to_i,
19
+ # '3 days before' => 3.days.to_i,
20
+ # '4 days before' => 4.days.to_i,
21
+ # '5 days before' => 5.days.to_i,
22
+ # '6 days before' => 6.days.to_i,
23
+ # '1 week before' => 1.weeks.to_i,
24
+ # '2 weeks before' => 2.weeks.to_i,
25
+ # '3 weeks before' => 3.weeks.to_i,
26
+ # '1 month before' => 1.month.to_i
27
+ # }
28
+
29
+ # REMINDERS = {
30
+ # '1 hour after' => 1.hours.to_i,
31
+ # '3 hours after' => 3.hours.to_i,
32
+ # '6 hours after' => 6.hours.to_i,
33
+ # '12 hours after' => 12.hours.to_i,
34
+ # '1 day after' => 1.days.to_i,
35
+ # '2 days after' => 2.days.to_i,
36
+ # '3 days after' => 3.days.to_i,
37
+ # '4 days after' => 4.days.to_i,
38
+ # '5 days after' => 5.days.to_i,
39
+ # '6 days after' => 6.days.to_i,
40
+ # '1 week after' => 1.weeks.to_i,
41
+ # '2 weeks after' => 2.weeks.to_i,
42
+ # '3 weeks after' => 3.weeks.to_i,
43
+ # '1 month after' => 1.month.to_i
44
+ # }
45
+
46
+ effective_resource do
47
+ category :string
48
+ reminder :integer # Number of seconds before event.start_at
49
+
50
+ # Email
51
+ from :string
52
+ subject :string
53
+ body :text
54
+
55
+ # Tracking background jobs email send out
56
+ started_at :datetime
57
+ completed_at :datetime
58
+
59
+ timestamps
60
+ end
61
+
62
+ scope :sorted, -> { order(:id) }
63
+ scope :deep, -> { includes(:event) }
64
+
65
+ scope :started, -> { where.not(started_at: nil) }
66
+ scope :completed, -> { where.not(completed_at: nil) }
67
+
68
+ # Called by a future event_notifier rake task
69
+ scope :notifiable, -> { where(started_at: nil, completed_at: nil) }
70
+
71
+ validates :category, presence: true, inclusion: { in: CATEGORIES }
72
+
73
+ validates :from, presence: true, email: true
74
+ validates :subject, presence: true
75
+ validates :body, presence: true
76
+
77
+ if EffectiveEvents.use_effective_email_templates
78
+ validates :body, liquid: true
79
+ validates :subject, liquid: true
80
+ end
81
+
82
+ # validates :reminder, if: -> { reminder? || upcoming_reminder? || before_event_ends? },
83
+ # presence: true, uniqueness: { scope: [:event_id, :category], message: 'already exists' }
84
+
85
+ def to_s
86
+ 'event notification'
87
+ end
88
+
89
+ def email_template
90
+ 'event_' + category.to_s.parameterize.underscore
91
+ end
92
+
93
+ def registrant_purchased?
94
+ category == 'Registrant purchased'
95
+ end
96
+
97
+ # def upcoming_reminder?
98
+ # category == 'Upcoming reminder'
99
+ # end
100
+
101
+ # def event_start?
102
+ # category == 'When event starts'
103
+ # end
104
+
105
+ # def reminder?
106
+ # category == 'Reminder'
107
+ # end
108
+
109
+ # def before_event_ends?
110
+ # category == 'Before event ends'
111
+ # end
112
+
113
+ # def event_end?
114
+ # category == 'When event ends'
115
+ # end
116
+
117
+ # def notifiable?
118
+ # started_at.blank? && completed_at.blank?
119
+ # end
120
+
121
+ def notify_now?
122
+ true
123
+
124
+ #return false unless notifiable?
125
+
126
+ # case category
127
+ # when 'When event starts'
128
+ # event.available?
129
+ # when 'When event ends'
130
+ # event.ended?
131
+ # when 'Upcoming reminder'
132
+ # !event.started? && event.start_at < (Time.zone.now + reminder)
133
+ # when 'Reminder'
134
+ # !event.ended? && event.start_at < (Time.zone.now - reminder)
135
+ # when 'Before event ends'
136
+ # !event.ended? && event.end_at.present? && event.end_at < (Time.zone.now + reminder)
137
+ # else
138
+ # raise('unexpected category')
139
+ # end
140
+ end
141
+
142
+ def notify!(force: false, event_registrants: nil)
143
+ return false unless (notify_now? || force)
144
+
145
+ # We send one email to each registrant
146
+ event_registrants ||= event.event_registrants.purchased
147
+
148
+ update_column(:started_at, Time.zone.now)
149
+
150
+ opts = { from: from, subject: subject, body: body }
151
+
152
+ event_registrants.each do |event_registrant|
153
+ EffectiveEvents.send_email(email_template, event_registrant, opts)
154
+ end
155
+
156
+ update_column(:completed_at, Time.zone.now)
157
+ end
158
+
159
+ end
160
+ end
@@ -58,6 +58,10 @@ module Effective
58
58
  "#{event_ticket} - #{last_first_name}"
59
59
  end
60
60
 
61
+ def name
62
+ "#{first_name} #{last_name}"
63
+ end
64
+
61
65
  def last_first_name
62
66
  "#{last_name}, #{first_name}"
63
67
  end
@@ -0,0 +1,44 @@
1
+ = effective_form_with(model: [:admin, event_notification], engine: true) do |f|
2
+ - if inline_datatable?
3
+ = f.hidden_field :event_id
4
+ - else
5
+ = f.select :event_id, Effective::Event.sorted.all
6
+
7
+ = f.select :category, Effective::EventNotification::CATEGORIES,
8
+ label: 'Send an email notification'
9
+
10
+ - # Render email templates
11
+ - Effective::EventNotification::CATEGORIES.each do |category|
12
+ - template = 'event_' + category.parameterize.underscore
13
+
14
+ = f.show_if :category, category do
15
+ = render "/admin/event_notifications/form_#{template}", f: f
16
+
17
+ - if f.object.category == category && f.object.persisted?
18
+ = f.text_field :from
19
+ = f.text_field :subject
20
+ = f.text_area :body, rows: 10
21
+
22
+ - elsif EffectiveEvents.use_effective_email_templates == false
23
+ = f.text_field :from, value: EffectiveResources.mailer_sender
24
+ = f.text_field :subject, value: ''
25
+ = f.text_area :body, rows: 10, value: ''
26
+
27
+ - else
28
+ - email_template = Effective::EmailTemplate.where(template_name: template).first!
29
+
30
+ = f.text_field :from, value: email_template.from
31
+ = f.text_field :subject, value: email_template.subject
32
+ = f.text_area :body, rows: 10, value: email_template.body
33
+
34
+ - if EffectiveEvents.use_effective_email_templates
35
+
36
+ %p The available variables are:
37
+
38
+ %ul
39
+ - Effective::EventNotification::EMAIL_TEMPLATE_VARIABLES.each do |variable|
40
+ %li {{ #{variable} }}
41
+
42
+ %small.text-muted Only a developer can add additional variables
43
+
44
+ = effective_submit(f)
@@ -0,0 +1,3 @@
1
+ .alert.alert-warning
2
+ %strong When registrant purchased
3
+ notifications will be sent to each registrant at time of purchase.
@@ -16,6 +16,12 @@
16
16
  - datatable = Admin::EffectiveEventProductsDatatable.new(event_id: event.id)
17
17
  = render_inline_datatable(datatable)
18
18
 
19
+ = tab 'Email' do
20
+ %p Click New to add one or more emails.
21
+
22
+ - datatable = Admin::EffectiveEventNotificationsDatatable.new(event_id: event.id)
23
+ = render_datatable(datatable, inline: true, simple: true)
24
+
19
25
  = tab 'Registrations' do
20
26
  %h2 Registrations
21
27
  - datatable = Admin::EffectiveEventRegistrationsDatatable.new(event_id: event.id)
@@ -17,7 +17,7 @@
17
17
  = link_to('Abandon registration', effective_events.event_event_registration_path(registration.event, registration), 'data-confirm': "Really delete #{registration}?", 'data-method': :delete, class: 'btn btn-danger')
18
18
  to register for another event.
19
19
 
20
- %hr
20
+ %hr
21
21
 
22
22
  %h2 Event Registrations
23
23
 
@@ -1,5 +1,5 @@
1
1
  .effective-event
2
- %table.table.effective-events-table
2
+ %table.table.effective-event-table
3
3
  %tbody
4
4
  %tr
5
5
  %td Starts
@@ -0,0 +1,17 @@
1
+ ---
2
+ subject: 'You have been registered for {{ event.title }}'
3
+ from: 'admin@example.com'
4
+ ---
5
+ Hello {{ registrant.name }},
6
+
7
+ Your ticket {{ ticket.name }} has been purchased.
8
+
9
+ You are registered for {{ event.name }}
10
+
11
+ The event starts on {{ event.date }}
12
+
13
+ {{ event.url }}
14
+
15
+ Thank you.
16
+
17
+ Please contact us for assistance.
@@ -18,4 +18,15 @@ EffectiveEvents.setup do |config|
18
18
 
19
19
  # Events can be restricted by role
20
20
  config.use_effective_roles = true
21
+
22
+ # Mailer Configuration
23
+ #
24
+ # Additional mailer settings for parent_mailer, delivery_method, layout, sender and admin
25
+ # are required fields in config/initializers/effective_resources.rb
26
+
27
+ # Configure the class responsible to send e-mails.
28
+ # config.mailer = 'Effective::EventsMailer'
29
+
30
+ # Use effective email templates for event notifications
31
+ config.use_effective_email_templates = true
21
32
  end
data/config/routes.rb CHANGED
@@ -21,6 +21,7 @@ EffectiveEvents::Engine.routes.draw do
21
21
  resources :event_registrants, except: [:show]
22
22
  resources :event_addons, except: [:show]
23
23
  resources :event_registrations, only: [:index, :show]
24
+ resources :event_notifications, except: [:show]
24
25
  end
25
26
 
26
27
  end
@@ -131,5 +131,22 @@ class CreateEffectiveEvents < ActiveRecord::Migration[6.0]
131
131
  add_index <%= @event_registrations_table_name %>, :status
132
132
  add_index <%= @event_registrations_table_name %>, :token
133
133
 
134
+ create_table <%= @event_notifications_table_name %> do |t|
135
+ t.references :event
136
+
137
+ t.string :category
138
+ t.integer :reminder
139
+
140
+ t.string :from
141
+ t.string :subject
142
+ t.text :body
143
+
144
+ t.datetime :started_at
145
+ t.datetime :completed_at
146
+
147
+ t.datetime :updated_at
148
+ t.datetime :created_at
149
+ end
150
+
134
151
  end
135
152
  end
@@ -1,3 +1,3 @@
1
1
  module EffectiveEvents
2
- VERSION = '0.1.10'.freeze
2
+ VERSION = '0.2.0'.freeze
3
3
  end
@@ -9,7 +9,8 @@ module EffectiveEvents
9
9
  :events_table_name, :event_registrants_table_name, :event_tickets_table_name,
10
10
  :event_registrations_table_name, :event_products_table_name, :event_addons_table_name,
11
11
  :layout, :per_page, :use_effective_roles,
12
- :event_registration_class_name
12
+ :event_registration_class_name,
13
+ :mailer, :use_effective_email_templates
13
14
  ]
14
15
  end
15
16
 
@@ -19,4 +20,8 @@ module EffectiveEvents
19
20
  event_registration_class_name&.constantize || Effective::EventRegistration
20
21
  end
21
22
 
23
+ def self.mailer_class
24
+ mailer&.constantize || Effective::EventsMailer
25
+ end
26
+
22
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: effective_events
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code and Effect
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-17 00:00:00.000000000 Z
11
+ date: 2022-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -164,6 +164,20 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: effective_email_templates
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
167
181
  description: Event registrations, tickets and products
168
182
  email:
169
183
  - info@codeandeffect.com
@@ -180,6 +194,7 @@ files:
180
194
  - app/assets/stylesheets/effective_events.scss
181
195
  - app/assets/stylesheets/effective_events/base.scss
182
196
  - app/controllers/admin/event_addons_controller.rb
197
+ - app/controllers/admin/event_notifications_controller.rb
183
198
  - app/controllers/admin/event_products_controller.rb
184
199
  - app/controllers/admin/event_registrants_controller.rb
185
200
  - app/controllers/admin/event_registrations_controller.rb
@@ -188,6 +203,7 @@ files:
188
203
  - app/controllers/effective/event_registrations_controller.rb
189
204
  - app/controllers/effective/events_controller.rb
190
205
  - app/datatables/admin/effective_event_addons_datatable.rb
206
+ - app/datatables/admin/effective_event_notifications_datatable.rb
191
207
  - app/datatables/admin/effective_event_products_datatable.rb
192
208
  - app/datatables/admin/effective_event_registrants_datatable.rb
193
209
  - app/datatables/admin/effective_event_registrations_datatable.rb
@@ -198,14 +214,18 @@ files:
198
214
  - app/datatables/effective_event_registrations_datatable.rb
199
215
  - app/datatables/effective_events_datatable.rb
200
216
  - app/helpers/effective_events_helper.rb
217
+ - app/mailers/effective/events_mailer.rb
201
218
  - app/models/concerns/effective_events_event_registration.rb
202
219
  - app/models/effective/event.rb
203
220
  - app/models/effective/event_addon.rb
221
+ - app/models/effective/event_notification.rb
204
222
  - app/models/effective/event_product.rb
205
223
  - app/models/effective/event_registrant.rb
206
224
  - app/models/effective/event_registration.rb
207
225
  - app/models/effective/event_ticket.rb
208
226
  - app/views/admin/event_addons/_form.html.haml
227
+ - app/views/admin/event_notifications/_form.html.haml
228
+ - app/views/admin/event_notifications/_form_event_registrant_purchased.html.haml
209
229
  - app/views/admin/event_products/_form.html.haml
210
230
  - app/views/admin/event_registrants/_form.html.haml
211
231
  - app/views/admin/event_tickets/_form.html.haml
@@ -237,6 +257,7 @@ files:
237
257
  - app/views/effective/events/_spacer.html.haml
238
258
  - app/views/effective/events/index.html.haml
239
259
  - app/views/effective/events/show.html.haml
260
+ - app/views/effective/events_mailer/event_registrant_purchased.liquid
240
261
  - config/effective_events.rb
241
262
  - config/routes.rb
242
263
  - db/migrate/01_create_effective_events.rb.erb