Welcome to the fifth part of “How to create your own Single Page Application Autoresponder with Rails 4 and Angular.js”.
This post we will be focused on the follow ups implementation, also known as email autoresponders.
But before starting with the code, let’s see why autoresponder emails are so useful and important.
Let’s say you’re a teacher that have an own blog, and is about improving your english skills powerfully, and then you create a list that automatically sends tips to your readers about the basic skills; then, once you capture the suscribers’ attention with this free content, you offer them a premium paid course with advanced techniques. Before you know it, your blog has just become your new business.
In case you’re wondering how looks what we’ll build in this part, I’ll attach some screenshots:
So, let’s get right to the follow ups implementation:
Let’s start by adding all the gems that we’ll be using in this post
Gemfile:
gem 'delayed_job_active_record', '~> 4.1.0'
gem 'delayed_job_web', '~> 1.2.10'
gem 'mandrill_mailer', '~> 1.1.0'
gem 'aasm', '~> 4.3.0'
gem 'liquid', '~> 2.6.1'
source 'https://rails-assets.org' do
gem 'rails-assets-angular-ui-sortable', '= 0.13.4'
gem 'rails-assets-jquery-ui'
end
A couple of things to talk about the code above:
- Delayed jobs: We are using this gem to create background jobs and as it works with activerecord, we don’t need to install any additional extra pieces of software, which means that we can use delayed jobs for free in any platforms, such as heroku.com; we are also including the delayed_job_web in order to manage the jobs by visiting a special url in our application, but as you’ll notice we are using active_job, which means that if you are using sidekiq or a different background worker, you can switch and just make some modifications to your job classes depending on your needs.
-
Aasm: we’d want to control the states of the scheduled emails sent to the subscribers. For that reason we’ll use the aasm gem.
-
Liquid: We’ll implement a basic version to include dynamic variables content in our follow up templates, so we can parse the information based on the subscribers’ information.
-
About the ui sortable and ui plugins, we’ll use them because we want to control the order in follow ups and we want to implement the option for sorting this messages by using a simple dragging interaction.
-
mandrill_mailer: an easy way to send emails by using a mandrill api.
backend code time:
Let’s start by adding all the logic from the backend side:
Here’re all the migrations that we’ll use:
rails generate model FollowUp title:text days_to_be_sent_after_previous:integer time_to_be_sent:datetime content:text email_list_id:integer
rails generate model FollowUpSchedule state follow_up_id:integer subscriber_id:integer run_at:datetime
rails generate modal SendWindow monday:boolean tuesday:boolean wednesday:boolean thursday:boolean friday:boolean saturday:boolean sunday:boolean hour:time
rails generate delayed_job:active_record
Let’s run the migration that we just added.
bundle exec rake db:migrate
Then, we are going to edit the follow_up model and add the method for reordering the follow ups position; as you see, we are using a different and optimized query for either postgress and mysql:
#app/models/follow_up.rb
class FollowUp > ActiveRecord::Base
# Associations
#
belongs_to :email_list
validates_presence_of :email_list_id,
:title,
:days_to_be_sent_after_previous
has_many :send_windows, dependent: :destroy
accepts_nested_attributes_for :send_windows, allow_destroy: true
### Callbacks #######
after_create :set_position_number
# Thanks to http://thepugautomatic.com/2008/11/rails-jquery-sortables/
# query for mysql
def self.mysql_reorder_position(ids)
update_all(
['position = FIND_IN_SET(id, ?)', ids.join(',')],
{ id: ids }
)
end
def self.reorder_position(ids)
db_adapter = ENV['DB_ADAPTER'] || 'postgress'
return self.postgress_reorder_position(ids) if db_adapter == 'postgress'
return self.mysql_reorder_position(ids) if db_adapter == 'mysql'
end
# Query for postgresSql
def self.postgress_reorder_position(ids)
self.where('id in (?)', ids)
.update_all(["position = (STRPOS(?, ','||lpad(cast(id as text), 20, '0')||',') - 1)/21 + 1", ", #{ids.map{|x| "%020d" % x }.join(',')},"])
end
def is_first_in_email_list?
FollowUp.where(email_list_id: self.email_list_id).order('position ASC').first.try(:id) == self.id
end
private
def set_position_number
last_question = FollowUp.where(email_list_id: self.email_list_id)
.where("id #{self.id}")
.order('position DESC').first
position_number = (last_question.try(:position)).to_i + 1
update_column(:position, position_number) unless position
end
end
We’ll use the FollowUpSchedule model to control the time when the follow ups notifications are sent and, as you will notice, we are using the aasm gem to control the right states transitions to avoid sending multiple times the same notification.
# app/models/follow_up_schedule.rb
require 'aasm'
class FollowUpSchedule < ActiveRecord::Base
include AASM
belongs_to :subscriber
belongs_to :follow_up
aasm column: 'state' do
state :created, initial: true
state :started
state :failed
state :completed
event :process do
transitions from: [:created, :failed], to: :started
end
event :error do
transitions from: :started, to: :failed
end
event :finish do
transitions from: :started, to: :completed
end
end
end
Let’s add the association bellow:
# app/models/email_list.rb
has_many :follow_ups, dependent: :destroy
Send Windows
The follow ups(autoresponders) are sent based on the number of days configured individually. When the users only select the number of days, the autoresponders will be sent any time between 0 – 24 hours during that day; However, when the users specify the time and day by using the Send Window feature that gives to the user more control of autoresponder scheduling, they can choose which days of the week and at what time of those days the autoresponder will be sent.
Let’s start by declaring the send window model which is the responsible to control these preferences.
# app/models/send_window.rb
class SendWindow < ActiveRecord::Base
belongs_to :follow_up
validates_presence_of :hour
after_save :set_hour_in_minutes
private
def set_hour_in_minutes
hour_string = self.hour
splitted_hour = hour_string.split(':')
convert_hour_minutes = splitted_hour.first.to_i * 60 + splitted_hour.second.to_i
# only update the column since we don't want to trigger the callbacks again
self.update_column(:hour_in_minutes, convert_hour_minutes)
end
end
As you see, we’ll save in ‘minutes’ the time that the user is selecting for each preference; later on, we’ll see more about this logic but we’re saving the data this way because it is easier to calculate when scheduling notifications.
Right after the user is added to an email list, we’ll start sending the follow ups notifications if there are any in the email list.
# app/models/subscriber.rb
after_create :process_follow_ups
private
# let schedule the follow ups according to the email list
def process_follow_ups
manager = Autoresponder::Jobs::FollowUpManagerJob.new(self.email_list.id)
manager.send_first_email_notification(self.id)
end
Creating and managing follow ups
In this controller, we are sorting the follow ups and, in that method, we are using an optimized sql query which is available either for mysql and postgress. We’ll also use nested attributes so we can update/delete/add send window elements included in the follow up when we perform any operation to it.
# app/controllers/api/v1/follow_ups_controller.rb
class Api::V1::FollowUpsController < Api::BaseController
before_action :find_email_list
before_action :find_follow_up, except: [:index, :create, :sort]
def index
follow_ups = @email_list.follow_ups.order('position ASC')
render_serialized(follow_ups, FollowUpSerializer, root: false)
end
def create
follow_up = @email_list.follow_ups.create(follow_up_params)
if follow_up.valid?
render_serialized(follow_up, FollowUpSerializer, root: false)
else
render_error_object(follow_up.errors.messages)
end
end
def show
render_serialized(@follow_up, FollowUpSerializer, root: false)
end
def update
if @follow_up.update(follow_up_params)
render_serialized(@follow_up, FollowUpSerializer, root: false)
else
render_error_object(@follow_up.errors.messages)
end
end
def destroy
if @follow_up.destroy
render_serialized(@follow_up, FollowUpSerializer, root: false)
else
render_with_error('An error occured while deleting the follow up message')
end
end
def destroy_send_window
if @follow_up && @follow_up.send_windows.find(params[:send_window_id]).destroy
render_json_message('The send window was deleted successfully')
else
render_with_error('An error occured while deleting the follow up message')
end
end
def sort
element_ids = params[:element_ids]
if element_ids && FollowUp.reorder_position(element_ids)
render_json_message('The elements were updated successfullly')
else
render_with_error('The element was not found')
end
end
private
def find_email_list
uuid = params['email_uuid']
@email_list = EmailList.find_by(secure_key: uuid)
end
def find_follow_up
id = params['id']
@follow_up = @email_list.follow_ups.find(id)
end
def follow_up_params
params.require(:follow_up)
.permit(
:title,
:content,
:days_to_be_sent_after_previous,
:time_to_be_sent,
:email,
:send_windows_attributes => [
:id,
:sunday,
:monday,
:tuesday,
:wednesday,
:thursday,
:friday,
:saturday,
:_destroy,
:hour
]
)
end
end
Mailers using mandrill
The follow up message mailer is the responsible to send the follow up notification and we are using a template created within mandrill panel, and we are also using Liquid::Template to replace some variables in the form of: {{email}} or {{name}} are the only available at this moment.
# app/mailers/follow_up_message_mailer.rb
class FollowUpMessageMailer < MandrillMailer::TemplateMailer
default from: 'heriberto.perez@magmalabs.io'
def send_email(follow_up, subscriber)
recipients = [
{
name: subscriber.name,
email: subscriber.email
}
]
mandrill_mail(
template: 'follow_ups',
subject: replace_placeholder_values(follow_up.title, subscriber),
to: recipients,
vars: {
'CONTENT' => replace_placeholder_values(follow_up.content, subscriber),
},
important: true,
inline_css: true,
)
end
def replace_placeholder_values(content, subscriber)
template = Liquid::Template.parse(content)
hash_values = find_placeholder_subscriber(subscriber)
template.render(hash_values)
end
def find_placeholder_subscriber(subscriber)
@placeholder_values ||= {
'name' => subscriber.name,
'email' => subscriber.email
}
end
end
Let’s modify the EmailListSerializer
We are basically just adding the follow_ups attribute
# app/serializers/email_list_serializer.rb
class EmailListSerializer < ActiveModel::Serializer
attributes :name,
:default_from,
:default_from_name,
:remind_people_message,
:company_organization,
:city,
:country,
:state_province,
:phone,
:created_at,
:address,
:secure_key,
:thank_you_page_url,
:already_subscribed_url,
:subscribers_count,
:follow_ups_count,
:id
def follow_ups_count
object.follow_ups.count
end
def subscribers_count
object.subscribers.count
end
end
The followUpSerializer looks pretty straighforward.
# app/serializers/follow_up_serializer.rb
class FollowUpSerializer < ActiveModel::Serializer
attributes :title,
:content,
:days_to_be_sent_after_previous,
:position,
:first_in_email_list,
:id
has_many :send_windows,
serializer: SendWindowSerializer,
root: :send_windows_attributes
def send_windows
object.send_windows.order('created_at DESC')
end
def first_in_email_list
object.is_first_in_email_list?
end
end
Here’s the SendWindowSerializer:
# app/serializers/send_window_serializer.rb
class SendWindowSerializer < ActiveModel::Serializer
attributes :id,
:sunday,
:monday,
:tuesday,
:wednesday,
:thursday,
:friday,
:saturday,
:hour
end
Here’s what the configuration for delayed job web looks like:
# config.ru
if Rails.env.production?
DelayedJobWeb.use Rack::Auth::Basic do |username, password|
username == ENV['USER_DELAYEDJOB'] && password == ENV['PASSWORD_DELAYEDJOB']
end
end
Let’s setup delayed jobs as the default backgroud worker:
# config/application.rb
config.active_job.queue_adapter = :delayed_job
# We are placing some classes inside the lib folder
config.autoload_paths 587,
:user_name => ENV['MANDRILL_USERNAME'],
:password => ENV['MANDRILL_API_KEY'],
:domain => 'heroku.com'
}
ActionMailer::Base.delivery_method = :smtp
MandrillMailer.configure do |config|
config.api_key = ENV['MANDRILL_API_KEY']
end
Create the new route for delayed_jobs_web panel and the follow ups resources:
In fact, this is how it looks the whole config/routes.rb file.
# config/routes.rb
Rails.application.routes.draw do
devise_for :users
root to: 'users#index'
match '/delayed_secret_job' => DelayedJobWeb, :anchor => false, via: [:get, :post]
constraints do
namespace :api, path: '/api' do
namespace :v1 do
resources :email_lists do
collection do
post 'subscribe/:email_uuid', to: 'email_lists#add_public_subscriber'
end
end
resources :subscribers do
collection do
post 'validate_email', to: 'subscribers#validate_email'
post 'validate_email_uniqueness', to: 'subscribers#validate_email_uniqueness'
end
end
resource :versions, only: :none do
collection do
post 'undo', to: 'versions#undo'
end
end
resources :follow_ups do
collection do
post '/sort', to: 'follow_ups#sort'
delete '/destroy_send_window/:id', to: 'follow_ups#destroy_send_window'
end
end
end
end
end
end
Scheduling follow ups by using a manager class:
# lib/autoresponder/follow_up_scheduler_manager.rb
module Autoresponder
class FollowUpSchedulerManager
MINUTES_AFTER_MIDNIGHT = 1
def initialize(email_list_id)
find_email_list(email_list_id)
end
def find_email_list(list_id)
@email_list ||= EmailList.find_by(id: list_id)
end
def process_follow_up(follow_up_id, subscriber_id, scheduled_id)
follow_up = FollowUp.find_by(id: follow_up_id)
scheduled = FollowUpSchedule.find_by(id: scheduled_id)
subscriber = Subscriber.find_by(id: subscriber_id)
# in case the subscriber was removed we are
# not sending the follow ups anymore
if follow_up && subscriber
begin
scheduled.process!
FollowUpMessageMailer.send_email(follow_up, subscriber).deliver
# By default we'd want to schedule the second notification if exists
schedule_next(follow_up.id, subscriber_id) if find_next_scheduled(follow_up.id)
scheduled.finish!
rescue => e
Rails.logger.error "================ Error while sending process follow ups #{e.inspect}============"
scheduled.error!
end
end
end
def process_first_follow_up(subscriber_id)
follow_ups = @email_list.follow_ups.order('position ASC')
subscriber = Subscriber.find_by(id: subscriber_id)
first_follow = follow_ups.first
# in case the subscriber was removed we are
# not sending the follow ups anymore
if first_follow && subscriber
FollowUpMessageMailer.send_email(first_follow, subscriber).deliver
# By default we'd want to schedule the second notification if exists
schedule_next(first_follow.id, subscriber_id) if find_next_scheduled(first_follow.id)
end
end
def find_next_scheduled(follow_up_id)
@next_followup ||= @email_list.follow_ups
.order('position ASC')
.where('id > ?', follow_up_id)
.first
end
def schedule_next(follow_up_id, subscriber_id)
next_follow_up = find_next_scheduled(follow_up_id)
if next_follow_up
follow_up_attributes = {
follow_up_id: next_follow_up.id,
subscriber_id: subscriber_id,
run_at: calculate_run_at(next_follow_up)
}
FollowUpSchedule.create(follow_up_attributes)
end
end
def calculate_run_at(follow_up)
find_next_calculated_week_dates(follow_up).each do |day|
@selected_day = day[:day_date]
day_name = Date::DAYNAMES[@selected_day.wday].downcase
@next_send_windows = follow_up.send_windows.where("#{day_name} = ?", true)
break if @next_send_windows.exists?
end
if @next_send_windows.count > 1
minutes_quantity = @next_send_windows.order('hour_in_minutes ASC')
.first
.hour_in_minutes
else
minutes_quantity = MINUTES_AFTER_MIDNIGHT
end
@selected_day + minutes_quantity.minutes
end
# NOTE: Add some rspec tests after setting up the suite
# Response example:
# follow_up.days_to_be_sent_after_previous => 1
# Date.today => Thu, 15 Oct 2015
#
# After running the method you should have the next
# days starting from tomorrow
# next_week_days => [{:day_date=>Fri, 16 Oct 2015},
# {:day_date=>Sat, 17 Oct 2015},
# {:day_date=>Sun, 18 Oct 2015},
# {:day_date=>Mon, 19 Oct 2015},
# {:day_date=>Tue, 20 Oct 2015},
# {:day_date=>Wed, 21 Oct 2015},
# {:day_date=>Thu, 22 Oct 2015}]
def find_next_calculated_week_dates(follow_up)
days_after = follow_up.days_to_be_sent_after_previous || 1
date_starts = Date.today + days_after.days
next_week_days = []
7.times do
next_week_days :environment do
timezone_now = Time.zone.now
starting_with = timezone_now - 8.hours
finishing_with = timezone_now - 20.minutes
oldest_followups = FollowUpSchedule.where(run_at: starting_with..finishing_with)
.where(state: 'created')
.includes(:follow_up)
oldest_followups.find_each do |scheduled|
manager = Autoresponder::Jobs::FollowUpManagerJob.new(scheduled.follow_up.email_list.id)
manager.send_email_notification(scheduled.follow_up_id, scheduled.subscriber_id, scheduled.id)
end
end
end
What Next?
Now, we are going to create all the frontend code.
Let’s start by including all the dependencies for application.js and application.css that will be used in the second part of this tutorial.
// app/assets/javascripts/application.js
//= require angular-ui-sortable
//= require jquery-ui
then we need to include the dependencies on our main app file:
// app/assets/javascripts/autoresponder/app.js
var AutoresponderApp = angular.module('AutoresponderApp', [
'ui.sortable',
.
.
.
Css styles modifications:
Let’s replace the id “#email-lists” for “.email-list” class, something like this:
# app/assets/stylesheets/modules/email_lists.scss
.articles {
.row {
border-bottom: 1px solid $corn_silk;
padding: 17px 0;
.data {
padding-left: 30px;
}
.inline-block {
display: inline-block;
}
.actions {
text-align: right;
font-size: 20px;
padding: 16px;
}
}
.row-hover {
&:hover {
background: #fafafa;
}
}
.sortable-row {
cursor: move;
}
}
and we will include the following css code at the bottom of the same css file :
.follow-up-form {
.interval-section {
label.title {
display: inline-block;
margin-right: 5px;
}
#title {
width: 100px;
display: inline-block;
}
}
}
Time for declaring the new routes(states):
Notice that I’m pasting the routes that have been changed/modified:
# app/assets/javascripts/autoresponder/states.js
.state("home", {
url: "/",
templateUrl: 'autoresponder/components/dashboard_home.html',
})
.state("email_list.details.follow_ups", {
url: "/follow_ups",
abstract: true,
controller: 'parentFollowUpsCtrl',
templateUrl: 'autoresponder/components/email_lists/details/follow_ups/main.html',
})
.state("email_list.details.follow_ups.list", {
url: "",
controller: 'followUpsCtrl',
templateUrl: 'autoresponder/components/email_lists/details/follow_ups/index.html',
})
.state("email_list.details.follow_ups.add", {
url: "/add",
controller: 'createFollowUpsCtrl',
templateUrl: 'autoresponder/components/email_lists/details/follow_ups/add.html',
})
.state("email_list.details.follow_ups.edit", {
url: "/edit/:followUpId",
controller: 'followUpDetailsCtrl',
templateUrl: 'autoresponder/components/email_lists/details/follow_ups/edit.html',
})
We are using a parent controller to place the common methods used within follow ups section:
# app/assets/javascripts/autoresponder/components/email_lists/details/follow_ups/parentFollowUpsCtrl.js
AutoresponderApp
.controller('parentFollowUpsCtrl', [
'$scope',
'Render',
'Utils',
'findCurrentEmailListInParent',
'ngDialog',
'FollowUpService',
function (
$scope,
Render,
Utils,
findCurrentEmailListInParent,
ngDialog,
FollowUpService
) {
$scope.validDaySelected = function(send_window) {
var anySelectedDay = send_window.monday || send_window.tuesday || send_window.wednesday || send_window.thursday || send_window.friday || send_window.saturday || send_window.sunday
return !anySelectedDay;
}
$scope.addSendWindowTo = function(follow_up) {
follow_up.send_windows_attributes.push({
sunday: false,
monday: false,
tuesday: false,
wednesday: false,
thursday: false,
friday: false,
saturday: false,
isNotPersisted: true,
})
};
$scope.removeSendWindowFrom = function(followUp, index, sendWindow) {
followUp.send_windows_attributes.splice(index, 1);
if(!sendWindow.isNotPersisted) {
destroyFromDb(sendWindow);
}
};
var destroyFromDb = function(sendWindow) {
var data,
followUpId,
removeElements,
sendWindowId;
followUpId = $scope.$stateParams.followUpId;
_data = {
email_uuid: findCurrentEmailListInParent.secure_key,
send_window_id: sendWindow.id
};
FollowUpService.destroySendWindow(followUpId, _data).then(function(successResponse) {
return Render.showGrowlNotification('success', successResponse.status);
}, function(errorResponse) {
return Render.showGrowlNotification('warning', 'An error occurred while removing the send window');
});
};
$scope.availableHours = [
'00:00',
'4:00',
'8:00',
'12:00',
'16:00',
'20:00',
'24:00',
]
}
])
The main followup template should look like this:
# app/assets/javascripts/autoresponder/components/email_lists/details/follow_ups/main.html.haml
.row
%ui-view
The following directive is created in order to make the boostrap group button work:
// app/assets/javascripts/autoresponder/components/common/globalDirectives.js
AutoresponderApp
.directive('checkboxWithChangeHandler', ['$timeout', function($timeout) {
return {
replace: false,
require: 'ngModel',
scope: false,
link: function (scope, element, attr, ngModelCtrl) {
$timeout( function(){
if(element[0].checked) {
$(element).parent().addClass('active')
}
}, 0);
$(element).change(function () {
scope.$apply(function () {
ngModelCtrl.$setViewValue(element[0].checked);
});
});
}
};
}]);
Here, we are adding a function that returns if a variable is not defined or nil.
// app/assets/javascripts/autoresponder/components/common/utils.js
AutoresponderApp.factory('Utils', function() {
return {
httpProtocolUrlFor: function(url_params) {
.
.
.
},
isDefined: function(obj) {
return angular.isDefined(obj) && obj !== null;
},
}
});
Let’s change the template message shown in the home state.
# app/assets/javascripts/autoresponder/components/dashboard_home.html.haml
%h1
Dashboard Home
As we are changing some of the follow ups routes, we might want to remove this one:
rm app/assets/javascripts/autoresponder/components/email_lists/details/follow_ups.html.haml
Time to declare the new templates for follow ups:
# app/assets/javascripts/autoresponder/components/email_lists/details/follow_ups/add.html.haml
%form.follow-up-form{:name => "FollowUpForm",
"ng-submit" => "createFollowUp()",
:novalidate => ""}
.row
.col-md-7
.form-group{'ng-class' => "{ 'has-error': FollowUpForm.title.$invalid }"}
%label{:for => "title"} Title
%input#title.form-control{:type => "text",
'required' => 'required',
'placeholder' => 'Hello {{name}}',
'name' => 'title',
"ng-model" => 'follow_up.title'}/
.form-group{'ng-class' => "{ 'has-error': FollowUpForm.content.$invalid }"}
%label{:for => "content"} Content
%textarea#content.form-control{ :rows => '10',
'required' => 'required',
'placeholder' => 'Thank you for...',
'name' => 'content',
"ng-model" => "follow_up.content"}
.col-md-5.interval-section
%h4
Interval
%div{'ng-show' => 'email_list.follow_ups_count == 0'}
Follow Up # 1 is the welcome message your subscribers will get immediately after signing up.
%div{'ng-show' => 'email_list.follow_ups_count != 0'}
%span.form-group{'ng-class' => "{ 'has-error': FollowUpForm.days_to_be_sent_after_previous.$invalid }"}
%label.title{:for => "title"} Follow Up # {{email_list.follow_ups_count + 1}} sent
%input#title.form-control{:type => "number",
'ng-required' => 'email_list.follow_ups_count != 0',
'name' => 'days_to_be_sent_after_previous',
'ng-class' => "{ 'has-error': FollowUpForm.days_to_be_sent_after_previous.$invalid }",
"ng-model" => 'follow_up.days_to_be_sent_after_previous'}/
day(s) after previous
%hr
%ng-include{'ng-show' => 'email_list.follow_ups_count != 0',
:src => "'autoresponder/components/email_lists/details/follow_ups/send_window_preferences.html'"}
.row
.col-md-6
%button{ type:'submit',
'ng-disabled' => "FollowUpForm.$invalid",
class: 'btn btn-success'}
Create Follow up
%button{ type:'button',
'ui-sref' => 'email_list.details.follow_ups.list',
class: 'btn btn-warning'}
Cancel
Creating new follow ups
the Angular controller:
// app/assets/javascripts/autoresponder/components/email_lists/details/follow_ups/createFollowUpsCtrl.js
AutoresponderApp
.controller('createFollowUpsCtrl', [
'$scope',
'Render',
'findCurrentEmailListInParent',
'FollowUpService',
function (
$scope,
Render,
findCurrentEmailListInParent,
FollowUpService
) {
$scope.follow_up = {
send_windows_attributes: []
}
$scope.createFollowUp = function() {
var _data;
_data = {};
_data.follow_up = $scope.follow_up;
_.extend(_data, { email_uuid: findCurrentEmailListInParent.secure_key });
FollowUpService.create(_data).then(function(follow_up) {
// We are using email_list_count when adding a new follow up
// in order to show/hide interval
$scope.email_list.follow_ups_count +=1;
Render.showGrowlNotification('success', 'The follow up was created successfully');
return $scope.$state.go('^.edit', { followUpId: follow_up.id });
}, function(errorResponse) {
return Render.showGrowlNotification('warning', 'An error occurred while creating the follow up');
});
}
}
])
Send windows options included when creating and editing a follow up:
# app/assets/javascripts/autoresponder/components/email_lists/details/follow_ups/send_window_preferences.html.haml
%h4 Send window
%div{'ng-repeat' => 'send_window in follow_up.send_windows_attributes'}
%div{'ng-show' => '!send_window._destroy'}
%div{class: 'pull-right'}
%a{:href => "", 'ng-click' => 'removeSendWindowFrom(follow_up, $index, send_window)'}
%span.glyphicon.glyphicon-trash{"aria-hidden" => "true"}
%h5 Time
.form-group{'ng-class' => "{ 'has-error': FollowUpForm.hour.$invalid }"}
%select.form-control{"ng-model" => "send_window.hour",
'required' => 'required',
'name' => 'hour',
"ng-options" => "hour as hour for hour in availableHours"}
%h5 Day
%br
%div{ 'ng-show' => "validDaySelected(send_window)" }
%h5.text-danger You must select at least one option
.btn-group{"data-toggle" => "buttons", 'data-validation' => "required"}
%label.btn.btn-primary
%input{'ng-model' => "send_window.sunday",
:type => "checkbox",
'name' => 'send_window.sunday',
'ng-required' => "validDaySelected(send_window)",
'checkbox-with-change-handler' => ''}/
Sun
%label.btn.btn-primary
%input{'ng-model' => "send_window.monday",
:type => "checkbox",
'name' => 'monday',
'checkbox-with-change-handler' => ''}/
Mon
%label.btn.btn-primary
%input{'ng-model' => "send_window.tuesday",
:type => "checkbox",
'name' => 'send_window.tuesday',
'checkbox-with-change-handler' => ''}/
Tue
%label.btn.btn-primary
%input{'ng-model' => "send_window.wednesday",
:type => "checkbox",
'name' => 'wednesday',
'ng-required' => "validDaySelected(send_window)",
'checkbox-with-change-handler' => ''}/
Wed
%label.btn.btn-primary
%input{'ng-model' => "send_window.thursday",
:type => "checkbox",
'name' => 'thursday',
'ng-required' => "validDaySelected(send_window)",
'checkbox-with-change-handler' => ''}/
Thu
%label.btn.btn-primary
%input{'ng-model' => "send_window.friday",
:type => "checkbox",
'ng-required' => "validDaySelected(send_window)",
'name' => 'friday',
'checkbox-with-change-handler' => ''}/
Fri
%label.btn.btn-primary
%input{'ng-model' => "send_window.saturday",
:type => "checkbox",
'name' => 'saturday',
'checkbox-with-change-handler' => ''}/
Sat
%hr
%button{ type:'button',
'ng-click' => 'addSendWindowTo(follow_up)',
class: 'btn btn-sm btn-success'}
Add new send window
The follow ups edition looks like:
# app/assets/javascripts/autoresponder/components/email_lists/details/follow_ups/edit.html.haml
%form.follow-up-form{:name => "FollowUpForm",
"ng-submit" => "updateFollowUp()",
:novalidate => ""}
.row
%h3
Follow Up Details
.col-md-7
.form-group{'ng-class' => "{ 'has-error': FollowUpForm.title.$invalid }"}
%label{:for => "title"} Title
%input#title.form-control{:type => "text",
'required' => 'required',
'name' => 'title',
"ng-model" => 'follow_up.title'}/
.form-group{'ng-class' => "{ 'has-error': FollowUpForm.content.$invalid }"}
%label{:for => "content"} Content
%textarea#content.form-control{ :rows => '10',
'required' => 'required',
'name' => 'content',
"ng-model" => "follow_up.content"}
.col-md-5.interval-section
%h4
Interval
%div{'ng-show' => 'follow_up.first_in_email_list'}
Follow Up # 1 is the welcome message your subscribers will get immediately after signing up.
%div{'ng-show' => '!follow_up.first_in_email_list'}
%span.form-group{'ng-class' => "{ 'has-error': FollowUpForm.days_to_be_sent_after_previous.$invalid }"}
%label.title{:for => "title"} Follow Up # {{email_list.follow_ups_count}} sent
%input#title.form-control{:type => "number",
'required' => 'required',
'name' => 'days_to_be_sent_after_previous',
'ng-class' => "{ 'has-error': FollowUpForm.days_to_be_sent_after_previous.$invalid }",
"ng-model" => 'follow_up.days_to_be_sent_after_previous'}/
day(s) after previous
%hr
%ng-include{'ng-show' => '!follow_up.first_in_email_list',
:src => "'autoresponder/components/email_lists/details/follow_ups/send_window_preferences.html'"}
.row
.col-md-6
%button{ type:'submit',
'ng-disabled' => "FollowUpForm.$invalid",
class: 'btn btn-success'}
Save settings
%button{ type:'button',
'ui-sref' => 'email_list.details.follow_ups.list',
class: 'btn btn-warning'}
Cancel
Follow up details
The responsible angular controller looks something like this:
# app/assets/javascripts/autoresponder/components/email_lists/details/follow_ups/followUpDetailsCtrl.js
AutoresponderApp
.controller('followUpDetailsCtrl', [
'$scope',
'Render',
'findCurrentEmailListInParent',
'FollowUpService',
function (
$scope,
Render,
findCurrentEmailListInParent,
FollowUpService
) {
var loadEmailListFollowUp = function() {
var data, followUpId;
_data = {
email_uuid: findCurrentEmailListInParent.secure_key
};
followUpId = $scope.$stateParams.followUpId;
return FollowUpService.findOne(followUpId, _data).then(function(follow_up) {
$scope.follow_up = follow_up.plain();
});
};
loadEmailListFollowUp();
$scope.updateFollowUp = function() {
var _data;
_data = {};
_data.follow_up = $scope.follow_up;
_.extend(_data, { email_uuid: findCurrentEmailListInParent.secure_key });
FollowUpService.update(_data, $scope.follow_up.id).then(function(follow_up) {
Render.showGrowlNotification('success', 'The follow up was updated successfully');
return $scope.follow_up = follow_up;
}, function(errorResponse) {
return Render.showGrowlNotification('warning', 'An error occurred while updating the follow up');
});
}
}
])
The follow up service responsible for making http requests against the server:
# app/assets/javascripts/autoresponder/components/email_lists/details/follow_ups/followUpService.js
AutoresponderApp
.factory('FollowUpService', ['Restangular',
function(
Restangular
) {
var model;
model = 'api/v1/follow_ups';
return {
create: function(params) {
return Restangular.all(model).post(params);
},
sort: function(params) {
// this will send the params to the endpoint post api/v1/questions/sort
return Restangular.all(model + '/sort').post(params);
},
findAll: function(params) {
return Restangular.all(model).customGET('', params);
},
destroy: function(followUpId, data) {
return Restangular.one(model, followUpId).remove(data);
},
destroySendWindow: function(followUpId, data) {
return Restangular.one(model + '/destroy_send_window', followUpId).remove(data);
},
findOne: function(followUpId, extra_params) {
return Restangular.one(model, followUpId).get(extra_params);
},
update: function(params, followUpId) {
return Restangular.one(model, followUpId).customPUT(params);
},
};
}
]);
listing follow ups:
The angular controller is going to look like this:
// app/assets/javascripts/autoresponder/components/email_lists/details/follow_ups/followUpsCtrl.js
AutoresponderApp
.controller('followUpsCtrl', [
'$scope',
'Render',
'Utils',
'findCurrentEmailListInParent',
'ngDialog',
'FollowUpService',
function (
$scope,
Render,
Utils,
findCurrentEmailListInParent,
ngDialog,
FollowUpService
) {
var loadEmailListFollowUps = function() {
var _data = {
email_uuid: findCurrentEmailListInParent.secure_key,
}
return FollowUpService.findAll(_data).then(function(messages) {
$scope.follow_up_messages = messages.plain();
});
};
loadEmailListFollowUps();
$scope.openModal = function(current_follow_up) {
$scope.current_follow_up = current_follow_up;
ngDialog.open({
template: 'autoresponder/components/email_lists/details/follow_ups/warningDestroy.html',
className: 'ngdialog-theme-default',
scope: $scope
});
};
$scope.destroyFollowUp = function(followUpId) {
var data;
_data = {};
_data.follow_up = $scope.follow_up;
_.extend(_data, { email_uuid: findCurrentEmailListInParent.secure_key });
FollowUpService.destroy(followUpId, _data).then(function(removed_list) {
Render.showGrowlNotification('success', 'The follow up was deleted sucessfully');
ngDialog.closeAll()
return $scope.$state.go('^.list', {}, { reload: true });
}, function(errorResponse) {
return Render.showGrowlNotification('warning', 'An error occurred while deleting the follow up');
});
}
$scope.sortableRows = {
// we are sending the new array with order
stop: function(e, ui) {
// we trigger the update only when the item has changed
if(Utils.isDefined(ui.item.sortable.dropindex)){
var newOrderElements,
elementsIds;
newOrderElements = ui.item.sortable.sourceModel;
elementIds = _.pluck(newOrderElements, 'id');
var _data = {};
_data.element_ids = elementIds;
_.extend(_data, { email_uuid: findCurrentEmailListInParent.secure_key });
FollowUpService.sort(_data).then(function(rows) {
Render.showGrowlNotification('success', 'The order was changed successfully');
}, function(errorResponse) {
Render.showGrowlNotification('warning', 'An error occurred while deleting the follow up');
});
}
},
};
}
])
Template responsible for listing follow ups will look like this:
// app/assets/javascripts/autoresponder/components/email_lists/details/follow_ups/index.html.haml
.articles
.row
%button{class: 'btn btn-success pull-right', 'ui-sref' => 'email_list.details.follow_ups.add'}
Create message
.follow-ups{'ui-sortable' => "sortableRows", 'ng-model' => 'follow_up_messages' }
.row{class: 'span12 sortable-row row-hover', "ng-repeat" => "message in follow_up_messages"}
.col-md-5
%h4
{{message.title}}
%p {{message.created_at}}
.col-md-4
.data.inline-block{"aria-hidden" => "true"}
%h4 0
%p.dim-el Opened
.data.inline-block{"aria-hidden" => "true"}
%h4 0.0%
%p.dim-el Opens
.col-md-3.actions
.btn-group
%button.btn.btn-default{:type => "button",
'ui-sref' => 'email_list.details.follow_ups.edit({ followUpId: message.id })'} Settings
%button.btn.btn-default.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", :type => "button"}
%span.caret
%span.sr-only Toggle Dropdown
%ul.dropdown-menu
%li
%a{:href => "", 'ng-click' => 'sendFollowUpTest(message.id)'} Send a test
%li.divider{:role => "separator"}
%li
%a{:href => "", 'ng-click' => 'openModal(message)'} Destroy
Before destroying a follow up we’d want to show a ‘destroy’ warning.
Let’s start by adding the templates.
# app/assets/javascripts/autoresponder/components/email_lists/details/follow_ups/warningDestroy.html.haml
.row
.row
.col-md-12
%h2.title
Are you sure you want to delete this follow up?
.row
.col-md-12
%h4
All the information related will be deleted as well
.row
.col-md-7
%label{'for' => 'delete_confirmation'}
Type DELETE to confirm
%input.form-control{ :name => "default_from_name",
:id => 'delete_confirmation',
'required' => 'required',
:placeholder => 'Type DELETE to confirm.',
'ng-model' => 'input_confirmation',
:type => "text",
:value => ""}/
%button.ngdialog-button.ngdialog-button-secondary{"ng-click" => "closeThisDialog(0)", :type => "button"} No
%button.ngdialog-button.ngdialog-button-primary{ "ng-click" => "destroyFollowUp(current_follow_up.id)",
'ng-class' => "{ 'ngdialog-button-secondary': input_confirmation != 'DELETE' }",
"ng-disabled" => "input_confirmation != 'DELETE'",
:type => "button" } Yes
What’s next? Now’s time for updating some old references to follow ups
from email list details:
# app/assets/javascripts/autoresponder/components/email_lists/details/main.html.haml
- %a{:href => "#", 'ui-sref' => 'email_list.details.follow_ups({ emailListId: email_list.id })'}
+ %a{:href => "#", 'ui-sref' => 'email_list.details.follow_ups.list({ emailListId: email_list.id })'}
# app/assets/javascripts/autoresponder/components/email_lists/index.html.haml
-#email-lists
+.articles
- %a{:href => "#", 'ui-sref' => 'email_list.details.follow_ups({ emailListId: list.id })'} Follow ups
+ %a{:href => "#", 'ui-sref' => 'email_list.details.follow_ups.list({ emailListId: list.id })'} Follow ups
- %a{:href => "#", 'ui-sref' => 'email_list.details.add_subscriber({ emailListId: list.id })'} Add Subscribers
+ %a{:href => "#", 'ui-sref' => 'email_list.details.subscribers.add({ emailListId: list.id })'} Add Subscribers
You can find the code in which we have been working on:
https://github.com/heridev/MailAssemble/tree/feature/implementation-part-5
Thanks for following my posts, I hope I’ll keep on writing some more, see you around.
H.