HOW ONCE A DAY SENDS AUTOMATED COOL POSTS TO YOUR EMAIL? (RAKE TASKS FOR EMAILS AND HEROKU SCHEDULER)

By: Saurav

2017-10-29 21:14:00 UTC

Hey! So recently I finished launching once a day for public use and asked few friends of mine to go check it out. If you also checked it out and I don't know you personally, I am really grateful. Please let me know what are your suggestions: tweet: sprakash24oct. If you feel like its cool and wanna join me in making onceaday better as an opensource project let me know.

In this post I want to talk about the algorithm and the scheduler once a day uses to send automated posts to those who signup (Did I mention, its free to post, read and get a cool post everyday in your inbox, so you can learn something cool in your busy schedule and need not browse through millions of websites? Sick ha! :) )

The requirement was to come up with an algorithm so users will get a randomly selected unread post in their inbox. This should be the featured post for the day for emails as well the main page. If all posts have been sent to users, don't send any old one.

Lets get started.

As you can imagine I have already existing Post Model and a User Model (Using Devise).

class Post < ApplicationRecord	

end

class User < ApplicationRecord  
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  validates_presence_of :name    

end

The idea was to:
1. Have another database table where I can store previously chosen post ids
2. Then compare which posts have not been yet chosen.
3. Take all those un-chosen posts and then randomly use one.
4. Add the id of that post to the chosen table and send the link as an email to the post to all users who have selected a yes on receive email.

For the chosen table, I created this migration:

rails g migration CreateMailedPost post_id:integer

and ran: rake db:migrate

I wanted to set up the task as a rake task so in the lib directory under tasks sub directory, I created a rake file: send_emails.rake

task :send_email => :environment do
		

end

Keep in mind, specifying :environment do is very important so rake task will have access to all the models and their methods.

The design porcess thought I had (which comes from reading cool books I blogged about) was to have minimal dependencies among classes, follow single responsibility principle as well as improving code reusability.

I decided to put methods which uses multiple classes in application helper module which I can include in the rake tasks later and included some class methods in respective models I would use in my rake task.

To give access to the class, specify the access route to the helper in the rake file:

require "#{Rails.root}/app/helpers/application_helper"

and then include all the methods in application helper module (Poodr by Sandi Metz has a really cool chapter 7 on using modules for inheritance, go read it to know flow of template pattern and how it works on the inside, I will summarize about it in another blog soon)

include ApplicationHelper

Few class methods I added to classes are:

In Post model:

def self.get_all_post_ids
  return Post.pluck(:id)
end

In User model:

def self.emails_of_all_intrested_users
	users = User.where(:receive_email => true).pluck(:email)
    return users
end

In MailedPost Model,

def self.get_all_mailedpost_ids
    return MailedPost.pluck(:post_id)
end

In application helper, the methods added are:


 def send_email(post, subject, message, user)
      from = 'yourpost@onceaday.today'          
      content = "<html><head><style type='text/css'>body,html,.body{background:#D3D3D3!important;}</style></head><body><container><spacer size='16'></spacer><row><columns><center><img class='small-float-center' width='500px' height='300px' src=#{Post.find_by_id(post.id).heroimage}></center></columns></row><row><columns large='8'><center><h2>Once A Day</h2></center></columns></row><row><columns large='6'><center><h4>Hey! Go on..just putting your selected post in your inbox for you to read</h4><br><p>#{message}</p><br><p>Follow: http://www.onceaday.today/subjects/#{post.subject_id}/posts/#{post.id} to learn something cool a user posted for today. </p><center></columns><columns large='6'><br><p>If you have any issues or suggestions, send me an email (just be nice!): </p><br><p>Email:sprakash24oct@gmail.com </p></columns><columns large='4'><img class='small-float-center' width='100px' height='100px' src='//s3-us-west-2.amazonaws.com/wacbacassetsdonttouch/wacbacassets/onceadaylogo.png' alt='once a day'></columns></row><row></row></container><body></html>"
      @notifier = EmailNotifier.new(from, user, subject, content)
      @notifier.send
end

which is the wrapper method to send emails. This method creates a new EmailNotifier objects and uses send method to send emails to the emails in user array.

Here is the emailnotifier.rb I created under lib directory:

require 'sendgrid-ruby'
require 'json'

class EmailNotifier

	include SendGrid

	attr_accessor :from, :to, :subject, :content

	def initialize(from, to, subject, content)
		@from = from
		@to = to
		@subject = subject
		@content = content		
	end


	def personalize		
		users = to

		email = Mail.new
		email.from = Email.new(email: @from)
		email.subject = subject

		personalization = Personalization.new	
		personalization.add_to(Email.new(email: "someemail"))

		users.each do |user|																
  			personalization.add_bcc(Email.new(email: user))					
		end

		email.add_personalization(personalization)
		email.add_content(Content.new(type: 'text/plain', value: "A new action was taken !!"))
		email.add_content( Content.new(type: 'text/html', value: content))
				
		email.reply_to = Email.new(email: 'someemail')
		
		return email
	end


	
	def send 
		use_sendgrid_to_send_email			
	end

	private

	def use_sendgrid_to_send_email
		begin	
				tosend = personalize

				sg = SendGrid::API.new(api_key: ENV['SENDGRID_API_KEY'])			
				response = sg.client.mail._('send').post(request_body: tosend.to_json)
				puts response.status_code
				puts response.body
				puts response.headers			
				return true
			rescue 			
				puts "Email Failure"			
				return false
			end	
		end
		
end

Remember, wherever your code interacts with an external api, wrap it in a begin recuse block. Also, its better to wrap it as only one method(Preferably private) in the class and let public methods interact with it so that in the future you know where to do the changes while public interface does not change at all (and thus not breaking anything)

To sign up with sendgrid, install gem 'sendgrid-ruby',

Use figaro to create database.yml and put your keys in there so you can access the keys using ENV

To add it on heroku, from command line, I use

heroku config:set TRIALAPI=my_api_name
heroku config:set SEND_GRID_API_KEY_ID=my_key_id
heroku config:set SENDGRID_API_KEY=my_key

Algorithm


Lets talk about how to choose the posts which have not been shared yet.

In ruby, if you have two arrays:

a = [1,2,3] and b = [1,2] 

and you want to find the element which are not common among them, you can use

a-b , which would give you [3]

To handle the cases of unknown length difference,

(a-b) | (b - a) will give you the array of elements not in common.

I used this logic to get ids of those posts which have not been yet added to mailed_post table and then randomly select one post, add it to the mailed_post table and emails the link to post to all the users:

The final rake task is below:

require "#{Rails.root}/app/helpers/application_helper"

include ApplicationHelper



task :send_email => :environment do
	
	posts = Post.get_all_post_ids
	mailed_posts = MailedPost.get_all_mailedpost_ids

	uncommon_posts_ids_to_randomize = (posts - mailed_posts) | (mailed_posts - posts)
	p uncommon_posts_ids_to_randomize

	if uncommon_posts_ids_to_randomize.size > 0	

		users = User.emails_of_all_intrested_users	

		post_id = uncommon_posts_ids_to_randomize.sample
		post = Post.find_by_id(post_id)

		begin			
			record = MailedPost.find_or_create_by(post_id: post_id)
	        send_email(post, "Go on..Just putting this cool post: #{post.title} in your inbox.", "Hi! When you are free have a look at : #{post.title}", users)
	    rescue
	    	p "email not sent "
	    end

		puts post_id

	end

end

In the figure below, you can see uncommon_posts_ids_to_randomize indicated as 1 and the randomly chosen post_id as 2

Schedulerraketask

Its imperative to test your rake task as well. You can use rspec for that. A cool tutorial is here

In the case of all posts added to the mailed_post table already no posts should be selected and thus no mail should be sent as indicated by 3 and 4 in the image below.

Schedulerraketask2

The final task is to set up a scheduler job to run this rake task every day. I started with whenever gem for cron jobs but was dissapointed because heroku doen't support cron jobs, it has its own sheduler.

The process is really simple as well:

First go to: Heroku Scheduler and add free heroku scheduler to your app.

Then go to you app page and if added it will show under overview tab with a link as shown below:

Schedulerpic

The link will bring you to scheduler manager console where you can add new task as explained below:

Herokuscheduler

You can specify the rake task to run at that time and it will run the task itself.

And that's how Once a day sends cool posts about what others are learning everyday to your email.

Schedulermail

Let me know if you have any suggestions or questions or feel like you can contribute: twitter -> sprakash24oct

Also, go use www.onceaday.today and start posting cool things you are learning today and learn from what others are learning as well. Its all free :)

(P.S. There is one scenario of sending posts with respect to each user's sign up date, that is a new user receiving all the old posts from start. Lets just say if you sign up later, you pay the penalty of not receiving old posts but you can always check them through browse link :) )

Owned & Maintained by Saurav Prakash

If you like what you see, you can help me cover server costs or buy me a cup of coffee though donation :)