Rails unsubscribe link with ActionMailer - ruby-on-rails

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

Related

Automatically emailing a user when they create a post

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

Rails4: What is the correct way to use form_for for submitting a hidden field

I am trying to pass a hidden field from a form whose value is derived from a text blob that user can edit on the webpage. (I use bootstrap-editable to let the user edit the blurb by clicking on it)
Here is the actual workflow:
User goes on 'Invitations page' where they are are provided with a form to enter friends email and shown a default text that will be used in the email
If the user want they can click on the text and edit it. This will make a post call via javascript to update_email method in Invitation controller
After the text is updated user is redirected back so now the user sees the same page with updated text. This works and user sees the updated text blurb instead of default [1-3] can happen any number of times
When the user submits the form , I expect to get the final version of email that I can save in the db and also trigger an email invitation to the users friend
Problem:
I keep getting default text from form parameters. Any idea what I am doing wrong?
Here is the form (Its haml instead of html)
#new-form
= form_for #invitation, :url=> invitations_path(), :html => {:class => 'form-inline', :role => 'form'} do |f|
.form-group
= f.text_field :email, :type=> 'email', :placeholder=> 'Invite your friends via email', :class=> 'form-control invitation-email'
= f.hidden_field :mail_text, :value => #invitation_email
= f.submit :class => 'btn btn-primary submit-email', :value => 'Send'
Here is the invitation controller:
class InvitationsController < ApplicationController
authorize_resource
before_filter :load_invitations, only: [:new, :index]
before_filter :new_invitation, only: [:new, :index]
before_filter :default_email, only: [:index]
#helper_method :default_email
def create
Invitation.create!(email: params[:invitation][:email], invited_by: current_user.id, state: 'sent', mail_text: params[:invitation][:mail_text], url: {referrer_name: current_user.name}.to_param)
redirect_to :back
end
def update_email
#invitation_email = params[:value]
flash[:updated_invitation_email] = params[:value]
redirect_to :back
end
private
def invitation_params
params.require(:invitation).permit!
end
def load_invitations
#invitations ||= current_user.sent_invitations
end
def new_invitation
#invitation = Invitation.new
end
def default_email
default_text = "default text"
#invitation_email = flash[:updated_invitation_email].blank? ? default_text : flash[:updated_invitation_email]
end
end
Assuming you are using Rails 4 then you need to permit the mail_text parameter:
class InvitationsController < ApplicationController
# ...
private
def invitation_params
params.require(:invitation).permit(:email, :mail_text) #...
end
end
Depending on your settings rails strong parameters will either raise an error or just silently null un-permitted params.
I have to say that your flow is a bit weird and that it may be better if you actually use a
more RESTful pattern:
1. User goes on 'Invitations page' where they are are provided with a form to enter friends email and shown a default text that will be used in the email
Send a AJAX POST request to /invitations (InvitationsController#create) it should return a JSON representation of the UNSENT invitation, store the returned invitation id on the form.
Note that you may need to setup the validations on your Invitation model so that it allows :email and :mail_text to be blank on creation
class Invitation < ActiveRecord::Base
validates :email, allow_blank: true
# ...
# Do full validation only when mail is being sent.
with_options if: :is_being_sent? do |invitation|
invitation.validates :email #...
invitation.validates :mail_text #...
end
# ...
def is_being_sent?
changed.include?("state") && state == 'sent'
end
end
2. User edits text
Send a AJAX PUT or PATCH request to /invitations/:id and update the invitation.
3. User clicks send
Send a POST request to /invitations/:id/send. Update the state attribute and validate.
If valid send invitation. Display a message to user.
class InvitationsController < ApplicationController
# ...
# POST /invitations/:id/send
def send
#invitation = Invitation.find(params[:id])
# Ensure we have latest values from form and trigger a more stringent validation
#invitation.update(params.merge({ state: :sent })
if #invitation.valid?
#mail = Invitation.send!
if #mail.delivered?
# display success response
else
# display error
end
else # record is invalid
# redirect to edit
end
end
# ...
end

Custom ActiveRecord errors for identical model / attribute, but different controller actions

In my Rails 4.0.2 application, an Account has_many Users, and each User belongs_to an Account. My Users can be created in one of two ways:
(a) Simultaneously with Account creation, by a post to Account#create. (Account#new displays a nested form which accepts attributes both for the Account and its first User.)
(b) In a post to User#create, made by a User with administrator privileges.
In both cases I'm validating the new User with validates :email, presence: true.
When validation fails in (a), I want to display the error message 'Please enter your email.'
When validation fails in (b), I want to display the error message 'Please enter the new user's email.'
In both cases I'm creating a User and using the same validation. The only difference is the controller action that initiates the User creation.
What's the best way to get my application to display two different error messages?
Make sure you are displaying flash messages on your page, and then just send the appropriate message as a flash message in your controller. Something like this:
class AccountsController < ApplicationController
def create
# code to build #account and #user, as a transaction
if #account.save
redirect_to wherever_you_want_url
else
if #user.errors.messages[:email] == ["can't be blank"]
flash.now[:notice] = "Please enter your email."
render :new
end
end
end
...
class UsersController < ApplicationController
before_filter check_privileges!, only: [:new, :create]
def create
# code to build #user
if #user.save
redirect_to wherever_you_want_url
else
if #user.errors.messages[:email] == ["can't be blank"]
flash.now[:notice] = "Please enter the new user's email."
render :new
end
end
end
Alright, after a bit of fumbling around here's my shot at it.
First, define a class variable in the User class:
class << self; attr_accessor :third_person end
Next, create a class method in the User class:
def self.third_person_helper(field, error)
return #third_person ? I18n.t("form.errors.third_person.#{field}.#{error}") : I18n.t("form.errors.first_person.#{field}.#{error}")
end
(Why a class variable and method? Because we'll be calling this from a validates statement, where I believe we've just got access to the class and not its instance. Trying to work with an instance method here just resulted in 'method not found' errors.)
Now, set up your two sets of error messages in your locale files:
en:
form:
errors:
third_person:
email:
blank: "this user's email can't be blank"
taken: "this user's email is already in use"
...
first_person:
email:
blank: "your email can't be blank"
taken: "your email is already in use"
...
Next, set up your validations like so, passing along the field and attribute you're validating:
validates :email, presence: { message: Proc.new { third_person_helper("email", "blank") } }
validates :email, presence: { message: Proc.new { third_person_helper("email", "taken") } }
...
Now that you've done that, you can switch to the third-person set of error messages just by setting User.third_person = true in your controller action, before you try and validate:
def create
# build the user here
User.third_person = true
if #user.save
# whatever you like
else
render :new
end
end
Finally, add this after_validation filter in your model, so you don't later get the third-person set of messages when you don't want them:
after_validation { User.third_person = false }
(If you want to avoid this filter, you can, but you'll have to call User.third_person = false in every controller action where you want to use the first-person set of messages.)
Whew. I like the solution I came up with because it doesn't clutter up the controllers with conditional code, but it's certainly more difficult to understand. If I had to program nicely with others I'd go the simpler route. I think it also violates Model-View-Controller best practices a bit by setting that model's class variable in the controller.
Because simpler's usually better, I'm accepting the other answer here as correct.

Catch Error from external API in Rails

I am building a simple web app that sends SMS messages to cell phones using Twilio. I want to ensure that the user has entered a full 10 digit phone number before it will allow a message to attempt to be sent.
When I test it with a less-than or greater-than 10 digit number, in heroku logs, I see Twilio::REST::RequestError (The 'To' number 1234567890 is not a valid phone number.).
I have tried to use a begin/rescue wrapper and am telling it to render text: "Try again with a valid number." and tried a variety of if statements to try to avoid the error.
I am pretty new to Ruby and Rails and Twilio, but I promise i have been through every guide I have found. Any help is greatly appreciated. Full code of my UserController below:
require 'twilio-ruby'
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = User.new(params[:user])
account_sid = '...'
auth_token = '...'
if #user.save
render text: "Wasn't that fun? Hit the back button in your browser to give it another go!"
begin
client = Twilio::REST::Client.new account_sid, auth_token
client.account.sms.messages.create(
from: '+16035093259',
to: #user.phone,
body: #user.message
)
rescue Twilio::REST::RequestError
render text: "Try again with a valid number."
end
else
render :new
end
end
end
I'd extract the SMS sending logic into a separate model/controller and use a background job to process the submitting. The UserController should only handle, well, user creation/modification.
Scaffolding:
$ rails g model sms_job user:references message:text phone submitted_at:datetime
$ rake db:migrate
Model:
class SmsJob < AR::Base
attr_accessible :user_id, :message, :phone
belongs_to :user
validates_presence_of :message, :phone, :user_id
validates :phone,
length: { min: 10 },
format: { with: /\+?\d+/ }
scope :unsubmitted, where(submitted_at: nil)
TWILIO = {
from_no: '...',
account_sid: '...',
auth_token: '...'
}
# find a way to call this method separately from user request
def self.process!
unsubmitted.find_each do |job|
begin
client = Twilio::REST::Client.new TWILIO[:account_sid], TWILIO[:auth_token]
client.account.sms.messages.create(
from: TWILIO[:from_no],
to: job.phone,
body: job.message
)
job.submitted_at = Time.zone.now
job.save
rescue Twilio::REST::RequestError
# maybe set update a tries counter
# or delete job record
# or just ignore this error
end
end
end
end
The controller then should just provide the information that the SMS is going to be send:
# don't forget the 'resources :sms_jobs' in your routes.rb
class SmsJobsController < ApplicationController
# index, update, destroy only for only admin?
def new
#sms_job = SmsJobs.new
end
def create
#sms_job = current_user.sms_jobs.build params[:sms_job]
if #sms_job.save
redirect_to root_url, notice: "Your message is being send!"
else
render :new
end
end
end
For the background processing, have a look at these excellent Railscasts :-) You probably need to workaround some concurrency problems if you have to process many messages and/or Twilio has a long response time (didn't use that service yet).

Advice on how to send email to multiple users based on an association

I have created a Ruby on Rails application where users can track workouts. The can do so either privately or publicly. On workouts which are public ( workout.share == 1 ) I allow users to comment. When a comment is created on a workout, the workout owner is notified via email. That all works great.
I am now looking for some advice on the best way to allow users who have commented on a workout, to also be notified via email. Here is an example.
User A creates Workout 1. User B comments on Workout 1 and User A receives an email notification. User C also comments on Workout 1 and both User A and User B receive email notifications.
What is the best way to tell my application to loop through all the users who have commented on Workout 1 and send an email to them?
Currently I am sending an email to the workout owner with the following code in the comments_controller (I realize this could be cleaner code):
class CommentsController < ApplicationController
...
def create
#workout = Workout.find(params[:workout_id])
#comment = #workout.comments.build(params[:comment])
#comment.user = current_user
respond_to do |format|
if #comment.save
if #comment.workout.email_notification == 1
#comment.deliver_comment_notification_mail!
format.html { redirect_to( projects_path) }
format.js
else
format.html { redirect_to( projects_path) }
format.js
end
else
end
end
end
...
and in comment_mailer.rb
def comment_notification_mail(comment)
subject "Someone commented on your Workout"
recipients("#{comment.workout.user.username} <#{comment.workout.user.email}>")
from("foobar")
body :comment => comment,
:commenter => comment.user,
:workout => comment.workout,
:commentee => comment.workout.user,
:workout_url => workout_url(comment.workout),
:commenter_url => user_url(comment.user)
end
To find out a workout owner and commenter is not a hard job. My suggestions are:
move the code of sending email in your controller to your model, using #after_create, eg:
class Comment < ActiveRecord::Base
#...
after_create :notify_subscribers
def subscribers
(self.workout.commenters << self.workout.owner).uniq
end
def notify_subscribers
#... implemented below
end
end
using delayed_job or other tools to put the email sending job to background, or the request would be blocked until all the emails has been sent. eg, in the #notify_owner_and_commenter method
def notify_subscribers
self.subscribers.each do |user|
CommentMailer.send_later :deliver_comment_notification_mail!(self, user)
end
end
Then you need to refactor you #deliver_comment_notification_mail! method with two arguments.
Delayed job ref: https://github.com/tobi/delayed_job
From my POV, it's all the work of the mailer. I'd just rewrite the comment_notification_mail to something more neutral (which could speak to workout owner and commenters).
Then something like:
def comment_notification_mail(comment)
recs = [comment.workout.user]
recs << comment.workout.comments(&:user)
recs -= comment.user
subject "Someone commented on your Workout"
recipients(recs.inject('') { |acc, r| "#{r.username} <#{r.email}>" })
from("foobar")
body :comment => comment,
:commenter => comment.user,
:workout => comment.workout,
:commentee => comment.workout.user,
:workout_url => workout_url(comment.workout),
:commenter_url => user_url(comment.user)
end
Of course, if mails are not supposed to be public, send by bcc ;)

Resources