0
votes

I am a trainee full stack developer learning ruby on rails and am in month 1 of a 6 month intensive course.

I am working on a 'reddit' style app where users can create topics, posts and comments.

I am trying to automatically email a user when they create a new post.

I am using ActionMailer for this.

I am working on an after_create callback in my post model and a method in a mailer called 'favorite_mailer'.

The problem I am facing, is that I am unable to successfully implement an after_create callback, which triggers an email to be automatically sent to a user after they create a post.

I have defined a method in my mailer called new_post, which should receive 2 arguments (user, post).

I have defined a callback method in my Post model called send_new_post email but can't make it pass my Rspec tests.

Any help will be greatly appreciated.

I have created the following Post model spec:

describe "send_new_post_email" do
  it "triggers an after_create callback called send_new_post_email" do
    expect(post).to receive(:send_new_post_email).at_least(:once)
    post.send(:send_new_post_email)
  end
   it "sends an email to users when they create a new post" do
     expect(FavoriteMailer).to receive(:new_post).with(user, post).and_return(double(deliver_now: true))
     post.save
   end
 end

Here is my Post Model (the relevant bit being the send_new_post_email callback):

class Post < ActiveRecord::Base
  belongs_to :topic
  belongs_to :user
  has_many :comments, dependent: :destroy
  has_many :votes, dependent: :destroy
  has_many :favorites, dependent: :destroy
  has_many :labelings, as: :labelable
  has_many :labels, through: :labelings
  after_create :create_vote
  after_create :create_favorite
  after_create :send_new_post_email
  default_scope { order('rank DESC') }


  validates :title, length: { minimum: 5 }, presence: true
  validates :body, length: { minimum: 20 }, presence: true
  validates :topic, presence: true
  validates :user, presence: true

  def up_votes
    votes.where(value: 1).count
  end

  def down_votes
    votes.where(value: -1).count
  end

  def points
    votes.sum(:value)
  end

  def update_rank
     age_in_days = (created_at - Time.new(1970,1,1)) / 1.day.seconds
     new_rank = points + age_in_days
     update_attribute(:rank, new_rank)
   end

   private

   def create_vote
     user.votes.create(value: 1, post: self)
   end

   def create_favorite
     user.favorites.create(post: self)
   end

   def send_new_post_email
     FavoriteMailer.new_post(self.user, self)
   end
end

Finally, here is my mailer:

class FavoriteMailer < ApplicationMailer
  default from: "[email protected]"

  def new_comment(user, post, comment)

 # #18
     headers["Message-ID"] = "<comments/#{comment.id}@your-app-name.example>"
     headers["In-Reply-To"] = "<post/#{post.id}@your-app-name.example>"
     headers["References"] = "<post/#{post.id}@your-app-name.example>"

     @user = user
     @post = post
     @comment = comment

 # #19
     mail(to: user.email, subject: "New comment on #{post.title}")
   end

   def new_post(user, post)

  # #18
      headers["Message-ID"] = "<post/#{post.id}@your-app-name.example>"
      headers["In-Reply-To"] = "<post/#{post.id}@your-app-name.example>"
      headers["References"] = "<post/#{post.id}@your-app-name.example>"

      @user = user
      @post = post

  # #19
      mail(to: user.email, subject: "You have favorited #{post.title}")
    end

end
1

1 Answers

0
votes

I would not use a model callback to handle these kind of lifecycle events.

Why?

Because it is going to be called whenever you create a record which means you will have to override it in your tests. ActiveRecord models can easily become godlike and bloated and notifying users is a bit beyond the models job of maintaining data and business logic.

It will also get in the way of delegating the notifications to a background job, which is very important if you need to send multiple emails.

So what then?

Well, we could stuff it in the controller. But that might not be optimal since controllers are PITA to test and we like 'em skinny.

So let's create an object with the single task of notifying the user:

module PostCreationNotifier
  def self.call(post)
    FavoriteMailer.new_post(post.user, post)
  end
end

And then we add it to the controller action:

def create
  @post = Post.new(post_params)
  if @post.save
    redirect_to @post
    PostCreationNotifier.call(@post)
  else
    render :new
  end
end

But - this probably is not what you want! Doh! It will only notify the creator that she just created a post - and she already knows that!

If we want to notify all the participants of a response we probably need to look at the thread and send an email to all of the participants:

module PostCreationNotifier
  def self.call(post)
    post.thread.followers.map do |f|
      FavoriteMailer.new_post(f, post)
    end
  end
end

describe PostCreationNotifier do

  let(:followers) { 2.times.map { create(:user) } }
  let(:post){ create(:post, thread: create(:thread, followers: followers)) }
  let(:mails) { PostCreationNotifier.call(post) }

  it "sends an email to each of the followers" do
    expect(mails.first.to).to eq followers.first.email
    expect(mails.last.to).to eq followers.last.email
  end
end

The pattern is called service objects. Having a simple object which takes care of a single task is easy to test and will make it easier to implement sending the emails in a background job. I'll leave that part to you.

Further reading: