I'm running into a problem when trying to create a new object using nested resources in Rails. My routing is set up as:
resources :coins do
resources :questions
resources :events
end
When I attempt to create a new event, it does not save. Adding ! to event.save gave me an error that says "Validation failed: Coin must exist".
I don't have a validation set up for the coin (in the Event model at least, if that is what its referring to). Checking the log file shows the following, which as far as I can tell shows that the correct coin is selected:
Started GET "/coins/1/events/new" for 127.0.0.1 at 2018-01-24 18:52:19 -0500
Processing by EventsController#new as HTML
Parameters: {"coin_id"=>"1"}
[1m[36mCoin Load (0.1ms)[0m [1m[34mSELECT "coins".* FROM "coins" WHERE "coins"."id" = ? LIMIT ?[0m [["id", 1], ["LIMIT", 1]]
[1m[36mUser Load (0.2ms)[0m [1m[34mSELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ?[0m [["id", 1], ["LIMIT", 1]]
Rendering events/new.html.erb within layouts/application
Rendered events/new.html.erb within layouts/application (3.9ms)
Rendered layouts/_rails_defaults.html.erb (34.8ms)
Rendered layouts/_shim.html.erb (0.6ms)
Rendered layouts/_header.html.erb (6.2ms)
Completed 200 OK in 67ms (Views: 62.3ms | ActiveRecord: 0.3ms)
Started POST "/coins/1/events" for 127.0.0.1 at 2018-01-24 18:54:46 -0500
Processing by EventsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"dAXQ3uwxR2IN2rbAYD//gulHebIOdZWFPYCKnxcUKTuV4QnUp+SZYHMpZUsGOgEXQjOAnhFUO9MpJkIIAKcQlQ==", "event"=>{"content"=>"LKNA", "link"=>"asd"}, "commit"=>"Submit", "coin_id"=>"1"}
[1m[36mCoin Load (0.2ms)[0m [1m[34mSELECT "coins".* FROM "coins" WHERE "coins"."id" = ? LIMIT ?[0m [["id", 1], ["LIMIT", 1]]
[1m[36mUser Load (0.2ms)[0m [1m[34mSELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ?[0m [["id", 1], ["LIMIT", 1]]
[1m[35m (0.1ms)[0m [1m[36mbegin transaction[0m
[1m[35m (0.0ms)[0m [1m[31mrollback transaction[0m
Completed 401 Unauthorized in 66ms (ActiveRecord: 3.0ms)
ActiveRecord::RecordInvalid (Validation failed: Coin must exist):
app/controllers/events_controller.rb:23:in `create'
I have it set up the same way for my Questions model, as they are almost identical aside from names, and that one works with no issues. I'm at a loss as to what I'm doing incorrectly here.
controller:
class EventsController < ApplicationController
before_action :find_event, only: [:show, :edit, :update, :destroy ]
before_action :find_coin
def index
#events = Event.where(coin_id: #coin.id).order("created_at DESC")
end
def show
end
def new
#event = current_user.events.build
end
def create
#event = current_user.events.build(event_params)
if #event.save!
flash[:success] = "Event Saved"
redirect_to coin_event_path(#coin.id, #event.id)
else
flash[:notice] = "Event *NOT* Saved!!!"
render 'new'
#event.errors.full_messages
end
end
.
.
.
private
def find_event
#event = Event.find(params[:id])
end
def find_coin
#coin = Coin.find(params[:coin_id])
end
def event_params
params.require(:event).permit(:content, :link)
end
end
model:
class Event < ApplicationRecord
belongs_to :user
belongs_to :coin
end
coin model:
class Coin < ApplicationRecord
validates :link_name, :currency_name, presence: true
has_many :questions
has_many :events
end
If anyone has any idea what I may be doing wrong or any tips on how to figure it out, I would really appreciate the assistance.
Making an answer for anyone else who needs it and doesn't read comments :)
When you do:
#event = current_user.events.build(event_params)
You actually create an event related for the user, and it is not related to the coin anymore.
Try to set the coin as well for the event before saving:
#event.coin = #coin
Or you can do it the other way - build the event on the current coin, and then set the user:
#event = #coin.events.build(event_params)
#event.user = current_user
That way - the coin_id will be set automatically because you build an event on that coin.
I have solved the same issue adding 'optional: true' to belongs_to method.
belongs_to :user, optional: true
By default 'belongs_to' require the user_id.
You can use too: 'required: false'.
belongs_to :user, required: false
Good luck.
Related
I'm using Rails 6 and ActionMailer. I'm working on sending a mail to the user everytime his post is commented on. Here's my code:
app/mailers/comment_mailer.rb
class CommentMailer < ApplicationMailer
def comment_mail
#user = params[:user]
mail(to: #user.email, subject: "Comments")
end
end
app/controllers/comments_controller.rb
class CommentsController < ApplicationController
before_action :authenticate_user!
def create
comment = Comment.new(comment_params)
comment.user_id = current_user.id
if comment.save
CommentMailer.with(user: #user).comment_mail.deliver_now
redirect_to post_path(comment.post.id)
end
end
app/views/comment_mailer/comment_mail.haml
%h4
Hi
- #user.name
%p Someone commented your post! Click the link below to see it:
(I haven't done the link step yet)
After following this tutorial: https://dev.to/morinoko/sending-emails-in-rails-with-action-mailer-and-gmail-35g4 I had this Error:
NoMethodError in CommentsController#create
undefined method `email' for nil:NilClass
My console:
NoMethodError (undefined method `email' for nil:NilClass):
app/mailers/comment_mailer.rb:5:in `comment_mail'
app/controllers/comments_controller.rb:8:in `create'
Started POST "/comments" for 172.17.0.1 at 2020-07-01 00:32:38 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1
Processing by CommentsController#create as HTML
Parameters: {"authenticity_token"=>"Mf15U/OG9DX3SKwoBRHE/D/xENBocTtdcS07aUur+p/tGJAWSxYSP65kovzhLXHXBjvs/Wzp2dV4/1+L4nxrdQ==", "comment"=>{"content"=>"well said!", "post_id"=>"2"}, "commit"=>"Comment"}
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 1], ["LIMIT", 1]]
(0.3ms) BEGIN
↳ app/controllers/comments_controller.rb:7:in `create'
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
↳ app/controllers/comments_controller.rb:7:in `create'
Post Load (0.4ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]]
↳ app/controllers/comments_controller.rb:7:in `create'
Comment Create (0.7ms) INSERT INTO "comments" ("content", "user_id", "post_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["content", "well said!"], ["user_id", 1], ["post_id", 2], ["created_at", "2020-07-01 00:32:38.439626"], ["updated_at", "2020-07-01 00:32:38.439626"]]
↳ app/controllers/comments_controller.rb:7:in `create'
(1.1ms) COMMIT
↳ app/controllers/comments_controller.rb:7:in `create'
CommentMailer#comment_email: processed outbound mail in 1.4ms
Completed 500 Internal Server Error in 54ms (ActiveRecord: 3.6ms | Allocations: 29044)
I used binding.pry to review the comment_mail method and I had #user >> nil as return.
When you call CommentMailer.with(user: #user).comment_mail.deliver_now
You haven't actually defined #user you mentioned that you have a user attached to the post record so use that
comment.post.user
so now you have
CommentMailer.with(user: comment.post.user).comment_mail.deliver_now
Or you make user a parameter of comment_mail
class CommentMailer < ApplicationMailer
def comment_mail(user)
#user = user #and this makes #user available in your templates
mail(to: #user.email, subject: "Comments")
end
end
And as you would expect call it like so
CommentMailer.comment_mail(comment.post.user).deliver_now #or preferably deliver_later
Assuming that comment belong_to posts, or whatever is being commented on, and then also assuming posts belong_to a user. You need to populate that #user variable. Something like:
#user = comment.post.user
To get the user of the post being commented on.
I think this should then work
CommentMailer.comment_mail(#user).deliver_now
and then update the method itself
def comment_mail(user)
#user = user
mail(to: #user.email, subject: "Comments")
end
You should also be able to then use the #user object in the mail view template too if you want to.
I can't understand why this is happening, seems that id of article is being queried even though friendly.id has been found. ID being found is "0" for any article that i click, even though the correct article_id (59) is found when associated with a comment.
Processing by ArticlesController#show as HTML
Parameters: {"id"=>"javascript-8"}
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 1], ["LIMIT", 1]]
Article Load (0.7ms) SELECT "articles".* FROM "articles" WHERE "articles"."slug" = $1 LIMIT $2 [["slug", "javascript-8"], ["LIMIT", 1]]
(1.9ms) BEGIN
Article Load (0.3ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" = $1 LIMIT $2 [["id", 0], ["LIMIT", 1]]
(1.1ms) ROLLBACK
Rendering articles/show.html.erb within layouts/application
Rendered comments/_comment_form.html.erb (19.3ms)
Comment Load (0.7ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = 59 ORDER BY created_at DESC
Rendered comments/_comment.html.erb (25.2ms)
EDITED: Articles controller
class ArticlesController < ApplicationController
before_action :authenticate_user!
before_action :set_article, only: [:show, :edit, :update, :destroy, :toggle_vote]
impressionist :actions=>[:show]
def show
#article_categories = #article.categories
#comments = Comment.where(article_id: #article).order("created_at DESC")
if #article.status == "draft" && #article.user != current_user
redirect_to root_path
end
end
private
def set_article
#article = Article.friendly.find(params[:id])
end
end
friendly_id :foo, use: :slugged # you must do
MyClass.friendly.find('bar')
or...
friendly_id :foo, use: [:slugged, :finders] # you can now do
MyClass.find('bar')
In your case
def set_article
#article = Article.friendly.find(params[:id])
end
It was because i was using impressionist gem.
I should remove
impressionist :actions=>[:show]
at top of controller, and instead add this in show
def show
impressionist(#article)
end
As written in impressionist gem usage guide (https://github.com/charlotte-ruby/impressionist#usage), "f you're using friendly_id be sure to log impressionist this way, as params[:id] will return a string(url slug) while impressionable_id is a Integer column in database. "
I've got Devise and omniauth gems. I more or less followed railscasts for multiple authentications so have Users and Authentications models.
I've managed to have a user update their own profile without a password when they have authentications.
class RegistrationsController < Devise::RegistrationsController
def update_resource(resource, params)
if current_user.authentications.empty?
resource.update_with_password(account_update_params)
else
params.except("current_password")
resource.update_without_password(account_update_params)
end
end
end
routes.rb:
devise_for :users, :controllers => {registrations: 'registrations'}, path_prefix: 'my'
resources :users
users/edit.html.haml
<%= bootstrap_form_for #user, html: {multipart: true} do |f| %>
...
<%= f.submit "Update" %>
users_controller.rb
def update
#user = User.find(params[:id])
Rails.logger.info(params.inspect)
if #user.update_without_password(account_update_params)
flash[:success] = "User updated by admin."
redirect_to #user
else
Rails.logger.info(#user.errors.inspect)
flash[:alert] = "User update by admin failed to save"
render 'edit'
end
private
def account_update_params
params.require(:user).permit(:email, :first_name, :last_name, :username, :dob,:city, :state, :zip, :password, :password_confirmation, :current_password)
end
end
user.rb
def update_without_password(params, *options)
params.delete(:password)
params.delete(:password_confirmation)
result = update_attributes(params, *options)
clean_up_passwords
result
end
Log file:
Started GET "/users/2/edit" for 127.0.0.1 at 2017-06-18 19:45:01 -0400
Processing by UsersController#edit as HTML
Parameters: {"id"=>"2"}
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
Rendering users/edit.html.erb within layouts/application
Rendered users/edit.html.erb within layouts/application (12.6ms)
Rendered layouts/_shim.html.erb (0.3ms)
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ? [["id", 1], ["LIMIT", 1]]
Rendered layouts/_sidenav.html.erb (1.0ms)
Rendered layouts/_nav_top.html.erb (0.4ms)
Rendered layouts/_notice.html.erb (0.3ms)
Rendered layouts/_footer.html.erb (0.2ms)
Completed 200 OK in 193ms (Views: 190.3ms | ActiveRecord: 0.3ms)
Started PATCH "/users/2" for 127.0.0.1 at 2017-06-18 19:45:04 -0400
Processing by UsersController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"SwkF8C+P6iExhc1ju91C9xFgaR5GSKwy8KFJUaBtHCTKzEXmPIKhaAFHlfMO+6u4/UOM0y2IAAIkpfTsuBOX6g==", "user"=>{"first_name"=>"Bobby", "last_name"=>"", "username"=>"", "dob"=>"", "gender"=>"", "address1"=>"", "address2"=>"", "state"=>"", "zip"=>"", "email"=>"test#test.com"}, "commit"=>"Update", "id"=>"2"}
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
<ActionController::Parameters {"utf8"=>"✓", "_method"=>"patch", "authenticity_token"=>"SwkF8C+P6iExhc1ju91C9xFgaR5GSKwy8KFJUaBtHCTKzEXmPIKhaAFHlfMO+6u4/UOM0y2IAAIkpfTsuBOX6g==", "user"=>{"first_name"=>"Bobby", "last_name"=>"", "username"=>"", "dob"=>"", "gender"=>"", "address1"=>"", "address2"=>"", "state"=>"", "zip"=>"", "email"=>"test#test.com"}, "commit"=>"Update", "controller"=>"users", "action"=>"update", "id"=>"2"} permitted: false>
(0.1ms) begin transaction
Authentication Exists (0.1ms) SELECT 1 AS one FROM "authentications" WHERE "authentications"."user_id" = ? LIMIT ? [["user_id", 2], ["LIMIT", 1]]
Authentication Exists (0.0ms) SELECT 1 AS one FROM "authentications" WHERE "authentications"."user_id" = ? LIMIT ? [["user_id", 2], ["LIMIT", 1]]
(0.0ms) rollback transaction
#<ActiveModel::Errors:0x007f9cf49d1758 #base=#<User id: 2, email: "test#test.com", created_at: "2017-06-18 16:05:51", updated_at: "2017-06-18 16:05:51", username: "", first_name: "Bobby", last_name: "", dob: nil, gender: "", address1: "", address2: "", city: nil, state: "", zip: "", admin: false, phone: nil>, #messages={:password=>["can't be blank"]}, #details={:password=>[{:error=>:blank}]}>
Rendering users/edit.html.erb within layouts/application
Rendered users/edit.html.erb within layouts/application (5.1ms)
Rendered layouts/_shim.html.erb (0.3ms)
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ? [["id", 1], ["LIMIT", 1]]
Rendered layouts/_sidenav.html.erb (0.9ms)
Rendered layouts/_nav_top.html.erb (0.5ms)
Rendered layouts/_notice.html.erb (0.3ms)
Rendered layouts/_footer.html.erb (0.2ms)
Completed 200 OK in 199ms (Views: 188.4ms | ActiveRecord: 0.7ms)
The message #messages={:password=>["can't be blank"] is pointing to the answer, but everything I've tried isn't removing this requirement. I thought that Devise's update_without_password method would work out of the box.
I've even tried this at the top in user.rb.
validates :password, presence: true, length: {minimum: 5, maximum: 120}, on: :create
validates :password, length: {minimum: 5, maximum: 120}, on: :update, allow_blank: true
Thanks for the help SO!
Check out your UsersController. You are using except on your paramsand then using account_update_params in the conditional. except doesn't exactly delete the key and value from the Hash.
This is a helpful SO answer that should fix your situation here .
If that doesn't help I would follow Devise's existing guide on how to achieve this:
Make sure you follow the Devise guide for allowing users to edit their account without providing a password. I'm not sure what you are trying to do in your User model, are you trying to overwrite the Devise method?
If you are trying to overwrite the Devise method you should look at the method itself:
def update_without_password(params, *options)
params.delete(:email)
super(params)
end
If you are using this method, you should probably override this method to protect other attributes you would not like to be updated without a password.
Change your RegistrationsController to the match the docs:
def update_resource(resource, params)
resource.update_without_password(params)
end
Then follow the remaining two steps and confirm your edited your view and routes.
If you need to make any changes authentication wise be sure to change the update_resource method.
I know there are a ton of questions like this, and a few wiki articles, but nothing seems to work.
They all seem to end up in some sort of redirect loop or something else like that.
This is the last thing I did, based on these instructions:
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
def after_sign_in_path_for(resource)
sign_in_url = new_user_session_url
if request.referer == sign_in_url
super
else
stored_location_for(resource) || request.referer || root_path
end
end
end
Yet this is what my logs shows happens when I sign in:
Started POST "/users/login" for ::1 at 2016-06-15 16:19:57 -0500
Processing by Devise::SessionsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"iGvdqYUdMgS/XXqTrh+XpW3PWQn473q/lm/lqsBeRcrcP+JRg==", "user"=>{"email"=>"abc#test.com", "password"=>"[FILTERED]", "remember_me"=>"0"}, "commit"=>"Login"}
User Load (4.1ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["email", "abc#test.com"], ["LIMIT", 1]]
(0.6ms) BEGIN
SQL (8.2ms) UPDATE "users" SET "current_sign_in_at" = $1, "last_sign_in_at" = $2, "sign_in_count" = $3, "updated_at" = $4 WHERE "users"."id" = $5 [["current_sign_in_at", 2016-06-15 21:19:57 UTC], ["last_sign_in_at", 2016-06-15 05:59:17 UTC], ["sign_in_count", 10], ["updated_at", 2016-06-15 21:19:57 UTC], ["id", 1546]]
(2.6ms) COMMIT
Redirected to http://localhost:3000/login
Completed 302 Found in 218ms (ActiveRecord: 15.6ms)
Started GET "/login" for ::1 at 2016-06-15 16:19:57 -0500
Processing by Devise::SessionsController#new as HTML
User Load (3.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 1546], ["LIMIT", 1]]
Redirected to http://localhost:3000/login
Filter chain halted as :require_no_authentication rendered or redirected
Completed 302 Found in 7ms (ActiveRecord: 3.6ms)
Started GET "/login" for ::1 at 2016-06-15 16:19:57 -0500
Processing by Devise::SessionsController#new as HTML
User Load (2.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 1546], ["LIMIT", 1]]
Redirected to http://localhost:3000/login
Filter chain halted as :require_no_authentication rendered or redirected
Completed 302 Found in 6ms (ActiveRecord: 2.5ms)
Started GET "/login" for ::1 at 2016-06-15 16:19:57 -0500
Processing by Devise::SessionsController#new as HTML
User Load (2.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 1546], ["LIMIT", 1]]
Redirected to http://localhost:3000/login
Filter chain halted as :require_no_authentication rendered or redirected
Completed 302 Found in 5ms (ActiveRecord: 2.0ms)
Started GET "/login" for ::1 at 2016-06-15 16:19:57 -0500
Processing by Devise::SessionsController#new as HTML
User Load (3.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 1546], ["LIMIT", 1]]
Redirected to http://localhost:3000/login
Filter chain halted as :require_no_authentication rendered or redirected
Completed 302 Found in 6ms (ActiveRecord: 3.0ms)
How do I redirect to the page the user is coming from after they have logged in (or registered) without getting this loop?
Edit 1
Based on the feedback from SsoulLess, this is what happens.
For starters, the infinite loop no longer happens - so that's good. However, it doesn't actually redirect to the right place.
When I add a binding.pry right into that after_sign_in_path_for(resource) method and explore my session hash, this is what I see:
[1] pry(#<Devise::SessionsController>)> session[:referer]
=> nil
[2] pry(#<Devise::SessionsController>)> session[:referrer]
=> nil
In theory, it should have a value, because of what I did. I was coming from a Question#Show view. Nothing to do with Users.
What else should store the previous path I am coming from before login?
Edit 2
Based on further suggestions from SsoulLess, this is what my code looks like now:
ApplicationController.rb
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
#aplication_controller.rb
def authenticate_user!(resource=nil)
session[:referer] = request.fullpath
redirect_to(new_user_registration_path, success: 'create an account or log in.') unless user_signed_in?
end
def after_sign_in_path_for(resource)
#Redirect back the user to the last action he wanted to do
if session[:referer] =~ /\/users/
root_path
elsif session[:referer].nil?
root_path
else
session[:referer]
end
end
end
And this is my QuestionsController
class QuestionsController < ApplicationController
before_action :set_question, only: [:edit, :update, :destroy]
before_action :set_voting_question, only: [:vote_up, :vote_down]
before_action :authenticate_user!, except: [:index, :show]
..
..
end
This is what my log looks like when I am viewing a Question#Show and need to login.
Started POST "/users/login" for ::1 at 2016-06-18 19:12:03 -0500
Processing by Devise::SessionsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"chcksZ9AMvZKvzJec5IMqwDhA58dYlB9tQy7OSY9dqqWw==", "user"=>{"email"=>"abc#test.com", "password"=>"[FILTERED]", "remember_me"=>"0"}, "commit"=>"Login"}
User Load (3.6ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["email", "abc#test.com"], ["LIMIT", 1]]
(0.7ms) BEGIN
SQL (6.0ms) UPDATE "users" SET "current_sign_in_at" = $1, "last_sign_in_at" = $2, "sign_in_count" = $3, "updated_at" = $4 WHERE "users"."id" = $5 [["current_sign_in_at", 2016-06-19 00:12:03 UTC], ["last_sign_in_at", 2016-06-15 21:19:57 UTC], ["sign_in_count", 11], ["updated_at", 2016-06-19 00:12:03 UTC], ["id", 1546]]
(0.8ms) COMMIT
Redirected to http://localhost:3000/
Completed 302 Found in 190ms (ActiveRecord: 11.1ms)
Started GET "/" for ::1 at 2016-06-18 19:12:03 -0500
Processing by QuestionsController#index as HTML
Rendering questions/index.html.erb within layouts/application
Rendered shared/_main_page_heading.html.erb (0.6ms)
It still doesn't return the user back to the Question#show page like I expect.
Edit 3
After trying what #sohair-ahmad suggested, it still doesn't work.
This is what I get when I drop a binding.pry in that method and explore the session object:
6: def after_sign_in_path_for(resource)
=> 7: binding.pry
8: session["user_return_to"] || root_path
9: end
[1] pry(#<Devise::SessionsController>)> session
=> #<ActionDispatch::Request::Session:0x007fd8baa9c078 ...>
[2] pry(#<Devise::SessionsController>)> session["user_return_to"]
=> nil
[3] pry(#<Devise::SessionsController>)> exit
Redirected to http://localhost:3000/
Completed 302 Found in 18434ms (ActiveRecord: 6.2ms)
The fundamental issue is that I can't find the attribute within session that stores the actual location before /login. If I could, then it would solve this.
Edit 4
Here is a Gist of my application_controller.rb and the log for the entire operation - https://gist.github.com/marcamillion/d7826b7289dc55bf4d33e688851bfc26
You will notice the first thing I did is I went to a Question#Show page. Then I logged out and it redirected me to /. Then from there, I went back to another question#show and then I logged in from there too. You will see the login process and then you will see the redirect to my root_path again. So it still isn't working, even after trying #SsoulLess's most recent updates.
Edit 5
After doing the latest suggestion from #SsoulLess, I get the functionality I would like for the most part....except when I go from /register -> /login and then I do login. It sends me back to /register and then keeps me in an infinite loop. See the logs below:
Started GET "/register" for 127.0.0.1 at 2016-06-25 23:45:38 -0500
Processing by Devise::RegistrationsController#new as HTML
Rendering devise/registrations/new.html.erb within layouts/application
Rendered devise/shared/_links.html.erb (2.1ms)
Rendered devise/registrations/new.html.erb within layouts/application (44.3ms)
Rendered shared/_navbar.html.erb (1.4ms)
Rendered shared/_footer.html.erb (0.3ms)
Completed 200 OK in 206ms (Views: 203.8ms | ActiveRecord: 0.0ms)
Started GET "/login" for 127.0.0.1 at 2016-06-25 23:45:42 -0500
Processing by Devise::SessionsController#new as HTML
Rendering devise/sessions/new.html.erb within layouts/application
Rendered devise/shared/_links.html.erb (1.5ms)
Rendered devise/sessions/new.html.erb within layouts/application (29.2ms)
Rendered shared/_navbar.html.erb (1.4ms)
Rendered shared/_footer.html.erb (0.5ms)
Completed 200 OK in 190ms (Views: 188.2ms | ActiveRecord: 0.0ms)
Started POST "/users/login" for 127.0.0.1 at 2016-06-25 23:45:48 -0500
Processing by Devise::SessionsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"cZ+kP5Q+rSuz3G/Jxwe0fPRSOnqJdA==", "user"=>{"email"=>"abc#test.com", "password"=>"[FILTERED]", "remember_me"=>"0"}, "commit"=>"Login"}
User Load (2.3ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["email", "abc#test.com"], ["LIMIT", 1]]
(0.8ms) BEGIN
SQL (1.4ms) UPDATE "users" SET "current_sign_in_at" = $1, "last_sign_in_at" = $2, "sign_in_count" = $3, "updated_at" = $4 WHERE "users"."id" = $5 [["current_sign_in_at", 2016-06-26 04:45:48 UTC], ["last_sign_in_at", 2016-06-26 04:45:04 UTC], ["sign_in_count", 27], ["updated_at", 2016-06-26 04:45:48 UTC], ["id", 1546]]
(0.8ms) COMMIT
Redirected to http://localhost:3000/register
Completed 302 Found in 168ms (ActiveRecord: 5.3ms)
Started GET "/register" for 127.0.0.1 at 2016-06-25 23:45:48 -0500
Processing by Devise::RegistrationsController#new as HTML
User Load (1.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 1546], ["LIMIT", 1]]
Redirected to http://localhost:3000/login
Filter chain halted as :require_no_authentication rendered or redirected
Completed 302 Found in 4ms (ActiveRecord: 1.3ms)
Started GET "/login" for 127.0.0.1 at 2016-06-25 23:45:48 -0500
Processing by Devise::SessionsController#new as HTML
User Load (1.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 1546], ["LIMIT", 1]]
Redirected to http://localhost:3000/login
Filter chain halted as :require_no_authentication rendered or redirected
Completed 302 Found in 4ms (ActiveRecord: 1.3ms)
This is the latest ApplicationController, how do I modify it so I don't get this issue:
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
after_action :store_location
def store_location
session[:previous_url] = request.referer
end
def after_sign_in_path_for(resource)
session[:previous_url] || root_path
end
end
You can use the session[:referer] it works with the latest version of Devise.
def after_sign_in_path_for(resource)
#Redirect back the user to the last action he wanted to do
if session[:referer] =~ /\/users/
root_path
elsif session[:referer].nil?
root_path
else
session[:referer]
end
end
Now as you can see above, if the referer is one of the /user/ page or the referee is nil, then we redirect to root_path otherwise we'll redirect to the referer, it means to the last action the user wanted to perform before log in.
Update
Yes Im sorry, I missed the part where you set the session[:referer] hash, you need to set it manually when getting in the log in page:
#aplication_controller.rb
def authenticate_user!(resource=nil)
session[:referer] = request.fullpath
redirect_to( new_user_registration_path, success: 'create an account or log in.') unless user_signed_in?
end
Update 2
the authenticate_user! method in your application controller is overriding the before_action :authenticate_user! filter, this is the way you use for request login for the actions within your controllers.
So when the user wants to perform an action he needs to log in, the before_action :authenticate_user! filter will redirect to the log in page, but the session[:referer] is set in the authenticate_user! function in your application controller. Then you can use it in the after_sign_in_path_for for redirect the user to the proper action the user wanted to perform before log in.
Update 3 - The final version that works! (almost, see below)
I understood what you want to achieve, you want to redirect the user back to the last page he visited before log in:
#aplication_controller.rb
after_action :store_location
def store_location
# store last url as long as it isn't a /users path
session[:previous_url] = request.referer
end
def after_sign_in_path_for(resource)
session[:previous_url] || root_path
end
Update 4 (this update is added by the OP)
For the most part, Update 3 worked, but there was one edge case that sent the app into an infinite loop. If the request.referer was one of the Devise actions upon login. See the updated question for a complete explanation of what happens.
Here is the complete solution that handles this edge case successfully:
def store_location
# store last url as long as it isn't a /users /register /login path
if request.referer =~ /\/users|\/login|\/register/
session[:previous_url] = root_path
elsif request.referer.nil?
session[:previous_url] = root_path
else
session[:previous_url] = request.referer
end
end
def after_sign_in_path_for(resource)
session[:previous_url] || root_path
end
I had the same issue and this A Simpler Solution? worked for me.
Adding snippet here
class ApplicationController < ActionController::Base
private
# If your model is called User
def after_sign_in_path_for(resource)
session["user_return_to"] || root_path
end
# Or if you need to blacklist for some reason
def after_sign_in_path_for(resource)
blacklist = [new_user_session_path, new_user_registration_path] # etc...
last_url = session["user_return_to"]
if blacklist.include?(last_url)
root_path
else
last_url
end
end
end
In my app, I give access to a customer to HIS own deals thanks to Cancan.
It works when I try it "manually" with the browser but I fail at implementing the rspec tests. A customer can't access other customer's deals but only his own (the administrator give him access through Active Admin interface).
It's like I am not managing to make rspec understand that the customer(through FactoryGirl) I create for tests should be allowed/associated with the deals I create for the tests (again through FactoryGirl).
THE TEST: /spec/controllers/deals_controller_spec.rb
require 'spec_helper'
require "cancan/matchers"
describe DealsController do
context "As signed-in CUSTOMER" do
before do
#customer = FactoryGirl.create(:customer) #the factory builds a basic customer i.e with 'prospect role' attributed by default
#deal = FactoryGirl.create(:deal, :customers => [#customer]) # in array as a deal has_many customers
sign_in_customer #customer
end
describe "the customer can read=view the page of a Deal HE OWNS " do
it "can access the page" do
get :deal_page, { :id => #deal.id }
expect(current_path).to eq(deal_page_path(#deal))
page.should have_content('Here is one of your deals, dear customer')
end
end
end
Here is the error I get:
DealsController As signed-in CUSTOMER with access to the deal page
Failure/Error: expect(current_path).to eq(deal_page_path(#deal))
expected: "/deals_page/2"
got: "/customer_interface_homepage"
(compared using ==)
Here is the detailed test log
Deal Exists (0.8ms) SELECT 1 AS one FROM "deals" WHERE LOWER("deals"."deal_code") = LOWER('CHA1FR001') LIMIT 1
SQL (2.1ms) INSERT INTO "deals" ("admin_user_id", "client_contact_point_name", blabla") VALUES ($1, $2, blabla...) RETURNING "id" [["admin_user_id", 1], ["client_contact_point_name", "henri Cool"], ["client_contact_point_profile_url", "http://example.com"], ....blabla...]
(...blabla)
Customer Exists (0.6ms) SELECT 1 AS one FROM "customers" WHERE (LOWER("customers"."email") = LOWER('person_1#example.com') AND "customers"."id" != 1) LIMIT 1
(...blabla)
Started GET "/customers/signin" for 127.0.0.1 at 2014-05-28 18:37:05 +0200
Processing by Customers::SessionsController#new as HTML
Rendered customers/sessions/new.html.erb within layouts/lightbox (40.0ms)
Rendered layouts/_metas.html.erb (0.4ms)
Rendered layouts/_messages.html.erb (0.7ms)
Rendered layouts/_footer.html.erb (1.2ms)
Completed 200 OK in 77ms (Views: 51.5ms | ActiveRecord: 0.0ms)
Started POST "/customers/signin" for 127.0.0.1 at 2014-05-28 18:37:05 +0200
Processing by Customers::SessionsController#create as HTML
Parameters: {"utf8"=>"✓", "customer"=>{"email"=>"person_1#example.com", "password"=>"[FILTERED]"}, "commit"=>"Log In"}
Customer Load (4.0ms) SELECT "customers".* FROM "customers" WHERE "customers"."email" = 'person_1#example.com' ORDER BY "customers"."id" ASC LIMIT 1
SQL (1.0ms) UPDATE "customers" SET "remember_created_at" = $1, "updated_at" = $2 WHERE "customers"."id" = 1 [["remember_created_at", 2014-05-28 16:37:05 UTC], ["updated_at", 2014-05-28 18:37:05 +0200]]
SQL (1.2ms) UPDATE "customers" SET "last_sign_in_at" = $1, "current_sign_in_at" = $2, "last_sign_in_ip" = $3, "current_sign_in_ip" = $4, "sign_in_count" = $5, "updated_at" = $6 WHERE "customers"."id" = 1 [["last_sign_in_at", 2014-05-28 16:37:05 UTC], ["current_sign_in_at", 2014-05-28 16:37:05 UTC], ["last_sign_in_ip", "127.0.0.1"], ["current_sign_in_ip", "127.0.0.1"], ["sign_in_count", 1], ["updated_at", 2014-05-28 18:37:05 +0200]]
**Redirected to http://www.example.com/customer_interface_homepage**
Completed 302 Found in 33ms (ActiveRecord: 6.2ms)
Started GET "/customer_interface_homepage" for 127.0.0.1 at 2014-05-28 18:37:05 +0200
Processing by ClientreportingPagesController#index as HTML
Customer Load (0.5ms) SELECT "customers".* FROM "customers" WHERE "customers"."id" = 1 ORDER BY "customers"."id" ASC LIMIT 1
(1.2ms) SELECT COUNT(*) FROM "roles" INNER JOIN "customers_roles" ON "roles"."id" = "customers_roles"."role_id" WHERE "customers_roles"."customer_id" = $1 AND (((roles.name = 'prospect') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL))) [["customer_id", 1]]
Rendered layouts/_metas.html.erb (0.2ms)
(0.8ms) SELECT COUNT(*) FROM "roles" INNER JOIN "customers_roles" ON "roles"."id" = "customers_roles"."role_id" WHERE "customers_roles"."customer_id" = $1 AND (((roles.name = 'superadmin') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL))) [["customer_id", 1]]
Rendered layouts/client_interface_partials
Completed 200 OK in 34ms (Views: 27.7ms | ActiveRecord: 2.4ms)
Processing by DealsController#deal_page as HTML
Parameters: {"id"=>"2"}
**Completed 401 Unauthorized in 1ms**
Rendered text template (0.1ms)
(0.5ms) ROLLBACK TO SAVEPOINT active_record_2
(0.3ms) ROLLBACK TO SAVEPOINT active_record_1
(0.3ms) ROLLBACK
I'm not sure it's the root cause of the issue but 2 things seem strange to me in this log:
why does rspec send to example.com/customer_interface_homepage (i have in my spec_helper file told rspec that i test locally: Capybara.asset_host = 'http:// localhost:3000') ?
why does rspec experience a "Completed 401 Unauthorized in 1ms at the end ?
Some files that might be useful to solve the issue:
/app/models/customer_ability.rb
class CustomerAbility
include CanCan::Ability
def initialize(customer)
alias_action :show, :to => :read #this will have no change on the alias :read!
customer ||= Customer.new # guest customer (not logged in)
if customer.has_role? :superadmin
Log.info "Ability: customer is superadmin"
can :manage, :all
else
can :read, Deal do |deal|
# Only customers who have been granted access in Active Admin to a deal can read
deal.customers.include? customer
end
end
end
end
controllers/deals_controller.rb
class DealsController < ApplicationController
before_filter :authenticate_customer!,
:only => [ :deal_page ]
def deal_page
#deal = Deal.find(params[:id])
authorize! :read, #deal # only allow customers with authorized access in AA; sends to customer_ability
respond_to do |format|
format.html
format.json { render json: #deal }
end
end
application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
# handle Cancan authorization exception
rescue_from CanCan::AccessDenied do |exception|
exception.default_message = t("errors.application_controller_exception_messages.only_open_to_admin")
if current_user # if it's user redirect to main HP
redirect_to root_path, :alert => exception.message
else # if it's a Customer redirect him to client interface HP
redirect_to customer_interface_homepage_path, :alert=> exception.message
end
end
def current_ability #inspired by http://mikepackdev.com/blog_posts/12-managing-devise-s-current-user-current-admin-and-current-troll-with-cancan
#current_ability ||= case
when current_user
UserAbility.new(current_user)
when current_customer
CustomerAbility.new(current_customer)
end
end
/spec/support/utilities.rb
include ApplicationHelper
def sign_in_customer(customer)
customer.confirm!
visit new_customer_session_path
fill_in "Email", with: customer.email
fill_in "Password", with: customer.password
click_on "Log In"
#populate cookie when not using capybara
cookies[:authentication_token] = customer.authentication_token
end
/spec/factories/deals.rb
FactoryGirl.define do
factory :deal do
# id i don't here any id
sequence(:deal_campaign_code) { |n| "CHA#{n}FR001" }
featured true
admin_user_id 1
end
end
/spec/factories/customers.rb
FactoryGirl.define do
factory :customer do # we use prospect as by definition a visitor signing in gets 'prospect status'
sequence(:email) { |n| "person_#{n}#example.com"}
password "bet(8a3#"
password_confirmation "bet(8a3#"
# required if the Devise Confirmable module is used
confirmed_at Time.now
confirmation_token nil
# create deals connected to the Customer
after(:create) do |customer|
customer.deals << FactoryGirl.create(:deal)
end
end
end
/app/models/customer.rb
class Customer < ActiveRecord::Base
rolify
# -- Relationships --------------------------------------------------------
has_many :customer_deals, dependent: :destroy
has_many :deals, through: :customer_deals
/app/models/deal.rb
class Deal < ActiveRecord::Base
# -- Relationships --------------------------------------------------------
belongs_to :admin_user, :foreign_key => 'admin_user_id'
has_many :customer_deals, dependent: :destroy
has_many :customers, through: :customer_deals
/app/models/customer_deal.rb
class CustomerDeal < ActiveRecord::Base
# -- Relationships --------------------------------------------------------
belongs_to :customer, :foreign_key => 'customer_id'
belongs_to :deal, :foreign_key => 'deal_id'
I think the session information is not being passed into the request you make in the spec. See this guide for how to use devise with controller tests.
As an alternative approach, I would recommend making this a feature spec instead of a controller spec. Note to make capybara play nice with devise follow this guide.
To answer your log questions:
Not sure why it says example.com/customer_interface_homepage but you can see on the next line that it is actually sending the GET request to 127.0.0.1/customer_interface_homepage. So that part is working. Perhaps your hosts file has example.com pointing to 127.0.0.1 and the first message is resolving the IP with DNS?
The 401 unauthorized looks to be due to your deal controller's authorize! call.
The spec is almost certainly failing due to current_path not being updated because of the 401 return from the deals controller. I believe if you fix that issue the current_path issue will also be fixed.
I have had problems in the past with FactoryGirl not actually writing models to the database on creation. This will cause anything relying on associations (which authorize! looks like it probably relies on due to CustomerAbility) to fail.
My suggestions moving forward are to debug authorize! (likely using log/puts statements) and figuring out what #deal actually looks like at that point as well as what deal.customers looks like. If you post the code to authorize! we may be able to help more.