Automatically emailing a user when they create a post - ruby-on-rails

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: "charlietarr1#gmail.com"
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

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:
http://www.sitepoint.com/comparing-background-processing-libraries-sidekiq/
https://blog.engineyard.com/2014/keeping-your-rails-controllers-dry-with-services

Related

How to implement retweet functionality in RoR?

I'm trying to implement retweet functionality on my app.
So I have my retweet_id in my tweets model
tweets schema
| user_id | content | created_at | updated_at | retweet_id
tweets.rb
belongs_to :user
has_many :retweets, class_name: 'Tweet', foreign_key: 'retweet_id'
user.rb
has_many :tweets
And in my tweets controller
tweets_controller.rb
...
def retweet
#retweet = Tweet.new(retweet_params)
if #retweet.save
redirect_to tweet_path, alert: 'Retweeted!'
else
redirect_to root_path, alert: 'Can not retweet'
end
end
Private
...
def retweet_params
params.require(:retweet).permit(:retweet_id, :content).merge(user_id: current_user.id)
end
In my view
tweets/show.html.erb
<%= link_to 'Retweet', retweet_tweet_path(#tweet.id), method: :post %>
My routes
resources :tweets do
resources :comments
resources :likes
member do
post :retweet
end
end
So when I try this I get an error
param is missing or the value is empty: retweet
So I remove .require from 'retweet_params' and that removes that error (though i'm unsure of how wise that is)
Then the link works but won't retweet - reverting to the fallback root_path specified in my action instead.
Unpermitted parameters: :_method, :authenticity_token, :id
Redirected to http://localhost:3000/
I'm not sure what i'm doing wrong. How can I get my retweets working? ty
The reason retweet_params raises an error is because your link link_to 'Retweet', retweet_tweet_path(#tweet.id), method: :post doesn't contain parameters like a new or edit form does. Instead you should create a new tweet that reference to tweet you want to retweet.
before_action :set_tweet, only: %i[show edit update destroy retweet]
def retweet
retweet = #tweet.retweets.build(user: current_user)
if retweet.save
redirect_to retweet, notice: 'Retweeted!'
else
redirect_to root_path, alert: 'Can not retweet'
end
end
private
def set_tweet
#tweet = Tweet.find(params[:id])
end
The above should automatically link the new tweet to the "parent". If this doesn't work for some reason you could manually set it by changing the above to:
retrweet = Tweet.new(retweet_id: #tweet.id, user: current_user)
The above approach doesn't save any content, since this is a retweet.
If you don't want to allow multiple retweets of the same tweet by the same user, make sure you have the appropriate constraints and validations set.
# migration
add_index :tweets, %i[user_id retweet_id], unique: true
# model
validates :retweet_id, uniqueness: { scope: :user_id }
How do we access the content of a retweet? The answer is we get the content form the parent or source (however you want to call it).
There is currently no association that lets you access the parent or source tweet. You currently already have:
has_many :retweets, class_name: 'Tweet', foreign_key: 'retweet_id'
To easily access the source content let's first add an additional association.
belongs_to :source_tweet, optional: true, inverse_of: :retweets, class_name: 'Tweet', foreign_key: 'retweet_id'
has_many :retweets, inverse_of: :source_tweet, class_name: 'Tweet', foreign_key: 'retweet_id'
With the above associations being set we can override the content getter and setter of the Tweet model.
def content
if source_tweet
source_tweet.content
else
super
end
end
def content=(content)
if source_tweet
raise 'retweets cannot have content'
else
super
end
end
# depending on preference the setter could also be written as validation
validates :content, absence: true, if: :source_tweet
Note that the above is not efficient when talking about query speed, but it's the easiest most clear solution. Solving parent/child queries is sufficiently difficult that it should get its own question, if speed becomes an issue.
If you are wondering why I set the inverse_of option. I would recommend you to check out the section Active Record Associations - 3.5 Bi-directional Associations.
Right now the error you're seeing is the one for strong params in Rails. If you can check your debugger or the HTTP post request that's being sent, you'd find that you don't have the params that you're "requiring" in retweet_params
def retweet_params
params.require(:retweet).permit(:retweet_id, :content).merge(user_id: current_user.id)
end
This is essentially saying that you expect a nested hash for the params like so
params = { retweet: { id: 1, content: 'Tweet' } }
This won't work since you're only sending the ID. How about something like this instead?
TweetsController.rb
class TweetsController < ApplicationController
def retweet
original_tweet = Tweet.find(params[:id])
#retweet = Tweet.new(
user_id: current_user.id,
content: original_tweet.content
)
if #retweet.save
redirect_to tweet_path, alert: 'Retweeted!'
else
redirect_to root_path, alert: 'Can not retweet'
end
end
end

Test Active Record Callback without hitting database

class Book < ActiveRecord::Base
belongs_to :author
end
class Author < ActiveRecord::Base
belongs_to :publisher
has_many :books
end
class Publisher < ActiveRecord::Base
before_destroy :remove_empty_author
has_many :authors, dependent: destroy_all
def remove_empty_author
books_present = authors.map(&:book).all? { |book| book.present? }
authors.destroy_all unless books_present
end
end
class PublishersController < ApplicationController
def destroy
#publisher = Publisher.find(params[:id].to_i)
#publisher.destroy # for simplicity I only show the destroy call
end
end
Scenario
This is so close to working!
I have a controller test.
I want to test the following without creating a database entry or querying the database:
1. The remove_empty_author call back is called & if I delete it & the before_destroy the test complains.
2. Destroy is called on author.
Note:
I do not want to test the details of the remove_empty_author method. This will be tested in a model unit test delete a publisher and all of the authors only if the author has no books.
What I have tried
describe "#delete," do
context "Publisher has no authors," do
before do
author = mock_model("Author", book: [])
#publisher = Publisher.new(id: 2, authors: [author, author])
current_publisher = mock_model("Publisher", id: 1)
allow(Publisher).to receive(:find).and_return(#publisher)
allow(controller).to receive(:current_publisher).and_return(current_publisher)
end
it "Controller calls destroy on Publisher" do
expect_any_instance_of(Publisher).to receive(:destroy)
end
it "Publisher calls remove_empty_author" do
expect_any_instance_of(Author).to receive(:destroy)
#publisher.run_callbacks(:destroy) do
false # Prevent active record from proceeding with destroy
end
end
end
end
Things that are working
Controller calls destroy on publisher test works.
The test successfully enters the remove_empty_author call back where
books_present = false,
authors = [(Double "Author_1001"), (Double "Author_1001")] authors.destroy_all = []
Error I am trying to overcome
Failure/Error:
DEFAULT_FAILURE_NOTIFIER = lambda { |failure, _opts| raise failure }
Exactly one instance should have received the following message(s) but didn't: destroy
What is not working
expect_any_instance_of(Author).to receive(:destroy)

Rails unsubscribe link with ActionMailer

I have an Email Model for users to sign up to receive email notifications when a article is added or updated. The email works properly but I'm receiving an error message with the unsubscribe method I've generated in my email.rb file. I found the unsubscribe solution within another stackoverflow question that was posted in 2012 but I'm not seeing how to work the solution correctly.
Email Model:
class Email < ActiveRecord::Base
validates :email, uniqueness: true
validates :email, presence: true
def unsubscribe
Email.find(params[:id]).update_attributes(permissions: false)
end
end
Article Model:
class Article < ActiveRecord::Base
...
has_many :emails
after_create :send_new_notifications!
after_update :send_update_notifications!
def send_update_notifications!
email = Email.where(permissions: true)
email.each do |email|
UpdatedArticleMailer.updated_article(email, self).deliver_later
end
end
def send_new_notifications!
email = Email.where(permissions: true)
email.each do |email|
ArticleNotificationMailer.new_article(email, self).deliver_later
end
end
end
Unsubscribe link in updated article email:
<%= link_to "Unsubscribe", email_url(#email.unsubscribe) %>
Error message:
undefined local variable or method `params' for #<Email:0x007ff5c2955e88>
def unsubscribe
Email.find(params[:id]).update_attributes(permissions: false)
end
end
You can't call params from a model. But moreover, you are calling the unsubscribe function while generating the view, which I don't think was our intention. Your setup should be:
In config/routes.rb
resources :emails do
get :unsubscribe, on: :member
end
This gets you a proper route to hit from your views.
In app/controllers/email_controller.rb
def unsubscribe
email = Email.find params[:id]
email.update_attributes(permissions: false)
... { handle errors, redirect on success, etc } ...
end
This handles flow of control.
In the view, the link becomes:
unsubscribe_email_url(email)
Essentially, the unsubscribe method moves to the controller. Should be pretty straightforward. Note that this call just generates the URL to be invoked when a user clicks the link, it doesn't actually make the call. Your current code is making the call.
params[:id] is only available in the controller.
Your link_to also doesn't make sense, it looks like you are trying to route to your model, those are not route-able. It should be a link to a controller action such as EmailsController#Unsubscribe and that URL will need an ID of some sort.
class EmailsController < ApplicationController
def unsubscribe
if email = Email.find(params[:id])
email.update_attribute(permissions: false)
render text: "You have been unsubscribed"
else
render text: "Invalid Link"
end
end
end
This does not take into account that you might want to use a token instead of an ID, in that case, see this article for using a MessageVerifier.
http://ngauthier.com/2013/01/rails-unsubscribe-with-active-support-message-verifier.html

testing with rspec codeschool level 5 challenge 3

Here is the base question for the test:
Update the spec so that whenever a tweet is created, we verify that email_tweeter is called on the tweet object. ***I can not alter the models, question, or mailer.***
Models:
# tweet.rb
class Tweet < ActiveRecord::Base
belongs_to :zombie
validates :message, presence: true
attr_accessible :message
after_create :email_tweeter
def email_tweeter
ZombieMailer.tweet(zombie, self).deliver
end
private :email_tweeter
end
# zombie.rb
class Zombie < ActiveRecord::Base
has_many :tweets
validates :email, presence: true
attr_accessible :email
end
Mailer:
class ZombieMailer < ActionMailer::Base
def tweet(zombie, tweet)
mail(:from => 'admin#codeschool.com',
:to => zombie.email,
:subject => tweet.message)
end
end
I keep bouncing around on this and could use a few pointers. Here is what I have been working with now: UPDATED
describe Tweet do
context 'after create' do
let(:zombie) { Zombie.create(email: 'anything#example.org') }
let(:tweet) { zombie.tweets.new(message: 'Arrrrgggghhhh') }
it 'calls "email_tweeter" on the tweet' do
tweet.email_tweeter.should_receive(:zombie)
tweet.save
end
end
end
And the error message is:
Failures:
1) Tweet after create calls "email_tweeter" on the tweet
Failure/Error: tweet.email_tweeter.should_receive(:zombie)
NoMethodError:
private method `email_tweeter' called for #<Tweet:0x000000062efb48>
# zombie_spec.rb:7:in `block (3 levels) '
Finished in 0.26328 seconds
1 example, 1 failure
Failed examples:
rspec zombie_spec.rb:6 # Tweet after create calls "email_tweeter" on the tweet
Any rspec peeps out there can point me in the right direction as to what I am missing here? Thank you.
How about this:
it 'calls "email_tweeter" on the tweet' do
tweet.should_receive(:email_tweeter)
tweet.save
end
do this
it 'calls "email_tweeter" on the tweet' do
tweet.email_tweeter.should_receive(:zombie)
tweet.save
end
Remove:
private :email_tweeter
You can't test private methods.
Update:
In fact you can test private methods (with send or eval methods which do not care about privacy), but you shouldn't, as those are part of implementation not the final output. In your tests you should rather save a new tweet an check that email has been sent. implementation details can change with time, it shouldn't affect tests as long as the mail is being send. You can for example try:
it 'generates and sends an email' do
tweet.save
ActionMailer::Base.deliveries.last.message.should eq tweet.message
end

ActiveRecord::RecordNotFound - in a descendant class' associated_controller#index

I am attempting to locate a parent object in a nested controller, so that I can associate the descendant resource with the parent like so:
# teams_controller.rb <snippet only>
def index
#university = Univeresity.find(params[:university_id])
#teams = #university.teams
end
When I call find(params[:university_id]) per the snippet above & in line 6 of teams_controller.rb, I receive ActiveRecord::RecordNotFound - Couldn't find University without an ID.
I'm not only interested in fixing this issue, but would also enjoy a better understanding of finding objects without having to enter a University.find(1) value, since I grant Admin the privilege of adding universities.
The Rails Guides say the following about the two kinds of parameters in a website:
3 Parameters
You will probably want to access data sent in by the user or other
parameters in your controller actions. There are two kinds of
parameters possible in a web application. The first are parameters
that are sent as part of the URL, called query string parameters. The
query string is everything after “?” in the URL. The second type of
parameter is usually referred to as POST data. This information
usually comes from an HTML form which has been filled in by the user.
It’s called POST data because it can only be sent as part of an HTTP
POST request. Rails does not make any distinction between query string
parameters and POST parameters, and both are available in the params
hash in your controller:
It continues a little further down, explaining that the params hash is an instance of HashWithIndifferentAccess, which allows usage of both symbols and strings interchangeably for the keys.
From what I read above, my understanding is that Rails recognizes both parameters (URL & POST) and stores them in the same hash (params).
Can I pass the params hash into a find method in any controller action, or just the create/update actions? I'd also be interested in finding a readable/viewable resource to understand the update_attributes method thats called in a controller's 'update' action.
Please overlook the commented out code, as I am actively searching for answers as well.
Thanks in advance.
Here are the associated files and server log.
Webrick
teams_controller.rb
class TeamsController < ApplicationController
# before_filter :get_university
# before_filter :get_team
def index
#university = University.find(params[:univeristy_id])
#teams = #university.teams
end
def new
#university = University.find(params[:university_id])
#team = #university.teams.build
end
def create
#university = University.find(params[:university_id])
#team = #university.teams.build(params[:team])
if #team.save
redirect_to [#university, #team], success: 'Team created!'
else
render :new, error: 'There was an error processing your team'
end
end
def show
#university = University.find(params[:university_id])
#team = #university.teams.find(params[:id])
end
def edit
#university = University.find(params[:university_id])
#team = #university.teams.find(params[:id])
end
def update
#university = University.find(params[:university_id])
#team = #university.teams.find(params[:id])
if #team.update_attributes(params[:team])
redirect_to([#university, #team], success: 'Team successfully updated')
else
render(:edit, error: 'There was an error updating your team')
end
end
def destroy
#university = University.find(params[:university_id])
#team = #university.teams.find(params[:id])
#team.destroy
redirect_to university_teams_path(#university)
end
private
def get_university
#university = University.find(params[:university_id]) # can't find object without id
end
def get_team
#team = #university.teams.find(params[:id])
end
end
team.rb
class Team < ActiveRecord::Base
attr_accessible :name, :sport_type, :university_id
has_many :home_events, foreign_key: :home_team_id, class_name: 'Event'
has_many :away_events, foreign_key: :away_team_id, class_name: 'Event'
has_many :medias, as: :mediable
belongs_to :university
validates_presence_of :name, :sport_type
# scope :by_university, ->(university_id) { where(team_id: team_id).order(name: name) }
# scope :find_team, -> { Team.find_by id: id }
# scope :by_sport_type, ->(sport_type) { Team.where(sport_type: sport_type) }
# scope :with_university, joins: :teams
# def self.by_university(university_id)
# University.where(id: 1)
# University.joins(:teams).where(teams: { name: name })
# end
def self.by_university
University.where(university_id: university_id).first
end
def self.university_join
University.joins(:teams)
end
def self.by_sport_type(sport_type)
where(sport_type: sport_type)
end
def self.baseball
by_sport_type('Baseball/Softball')
end
end
university.rb
class University < ActiveRecord::Base
attr_accessible :address, :city, :name, :state, :url, :zip
has_many :teams, dependent: :destroy
validates :zip, presence: true, format: { with: /\A\d{5}(-\d+)?\z/ },
length: { minimum: 5 }
validates_presence_of :name, :address, :city, :state, :url
scope :universities, -> { University.order(name: 'ASC') }
# scope :by_teams, ->(university_id) { Team.find_by_university_id(university_id) }
# scope :team_by_university, ->(team_id) { where(team_id: team_id).order(name: name)}
def sport_type
team.sport_type
end
end
views/teams/index.html.erb
Placed in gists for formatting reasons
rake routes output: (in a public gist)
enter link description here
rails console
You're not going to want to have both:
resources :universities #lose this one
resources :universities do
resources :teams
end
As for params... you have to give a param. So, when you go to http://localhost:3000/teams there are no params, by default. If you go to http://localhost:3000/teams/3 then params[:id] = 3 and this will pull up your third team.
Keep in mind the nomenclature of an index. The index action of Teams, is going to list all of the teams. All of them. There is no one University there, so what are you actually trying to find? If anything, you'd have, for your University controller:
def show
#university = University.find(params[:id])
#teams = #university.teams
end
so, the address bar will be showing http://localhost:3000/universities/23, right? params[:id] = 23, then you can find the teams associated with that university.

Resources