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
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
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.
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:
The link will bring you to scheduler manager console where you can add new task as explained below:
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.
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 :)