In my devise sign-up page, I have implemented an IP tracking feature that, when a user signs up, sends the country the user comes from, in order to populate the newly created account's attribute user_country.
I'd like to know if it's possible to implement a sort of validation on -user_country the same way I do for other User attributes (email, password...) when the user is created, to implement a sort of validation on user_country to be sure it's not empty nor nil, i.e that a user must always have a country identified.
I can't use the classic way with validates :user_country, presence: true, because when the user is created the user_country is still not filled but WAITS that RegistrationController makes a call to geocoder this way => see the method on the bottom called 'after_sign_up_path_for'
/app/controllers/registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController
layout 'lightbox'
def update
account_update_params = devise_parameter_sanitizer.sanitize(:account_update)
# required for settings form to submit when password is left blank
if account_update_params[:password].blank?
account_update_params.delete("password")
account_update_params.delete("password_confirmation")
end
#user = User.find(current_user.id)
if #user.update(account_update_params) # Rails 4 .update introduced with same effect as .update_attributes
set_flash_message :notice, :updated
# Sign in the user bypassing validation in case his password changed
sign_in #user, :bypass => true
redirect_to after_update_path_for(#user)
else
render "edit"
end
end
# for Rails 4 Strong Parameters
def resource_params
params.require(:user).permit(:email, :password, :password_confirmation, :current_password, :user_country)
end
private :resource_params
protected
def after_sign_up_path_for(resource)
resource.update(user_country: set_location_by_ip_lookup.country) #use concerns/CountrySetter loaded by ApplicationController
root_path
end
end
Is there a way to implement a sort of 'validate' in this kind of situation?
My understanding is you want a User model to validate user_country field but only during steps after the initial creation?
class User
validates_presence_of :user_country, on: :update
end
will only validate on update and not creation. See http://apidock.com/rails/ActiveRecord/Validations/ClassMethods/validates_presence_of for other options that you can add.
But for your use case, it seems odd that you need to do this in the controller after signup. I think it makes more sense to always validate user_country then inject the attribute during User creation in the controller.
Related
I'm having trouble implementing strong parameters, receiving the Undefined Method attr_accessible error locally.
Could anyone explain what exactly I have done wrong here.
users_controller.rb:
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
redirect_to root_url, :notice => "Signed up!"
else
render "new"
end
end
def user_params
params.require(:user).permit(:username, :email, :password, :password_confirmation)
end
end
And in user.rb:
class User < ActiveRecord::Base
attr_accessible :email, :password, :password_confirmation
has_secure_password
validates_presence_of :password, :on => :create
end
And perhaps a foolproof fix for this...I've tried a number of attempts but I just can't seem to get this right.
strong_params are usually done in the controller, not in the model. it's also described like this in the api. so, there's also no need for you to set attr_accesible. this way different controllers can also set different fields on a model, e.g. a backend users controller could be allowed to set an admin flag, while the users controller on the frontend is not allowed to do that.
so, your user_params method belongs in your UsersController, and the create and update action use user_params to filter out the params you don't allow to be set. e.g.:
#user = User.new(user_params)
Rails 4 uses strong params by default, and you don't need attr_accessible. Also in rails 4 you permit params in the controller instead of the model.
How is attr_accessible used in Rails 4?
I want to make my own simple authentication.
So I made User model and Users controller. And I have a form for sign up
Part of my Users controller.
def create
#user = User.new(user_params)
if #user.save
redirect_to root_path, notice: "You've successfully singed up."
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:username, :password, :password_confirmation)
end
And error:
unknown attribute: password
In my Users table I have username and password_digest for each User. So there're not column like password.
I've just watched Rails Cast episode but it is about Rails 3 and author uses attr_accessible but I've read in Rails 4 we should use strong parameters.
How I can deal with it?
It is likely that you are using has_secure_password and you have not enabled it correctly. has_secure_password adds two virtual attributes to your model: password and password_confirmation. The error unknown attribute could mean that those two attributes are not being set correctly on your model.
You should under no circumstances pass the password_digest as a param since BCrypt and has_secure_password use this column to compute a hashed version of your password attribute. In other words it has no business being in the form.
Make sure you have:
Included the BCrypt gem in your gemfile
Correct included the has_secure_password module in your user model.
Your use of strong_parameters is correct. The problem is in your model, likely to do with has_secure_password, and not strong_paramters. So this line is correct:
def user_params
params.require(:user).permit(:username, :password, :password_confirmation)
end
Strong parameters filter params to avoid mass assignments until they have been whitelisted.
The permit method identifies the list of allowed parameter and must correspond to your model's attributes.
So, in your case you should write
def user_params
params.require(:user).permit(:username, :password_digest)
end
In my devise sign-up page, i have implemented an ip tracking feature that, when a user signs up, sends the country the user comes from, in order to populate the newly created account's attribute 'user_country'
It works but as a newbie I don't know how to test with rspec that this works i.e that if i create a user who signs up, then AFTER account creation, that user_country is not empty any more.
Here how i use a /app/controllers/registrations_controller.rb method to assign a country to the user's newly created accoun on controllers/registrations_controller.rb- see below
class RegistrationsController < Devise::RegistrationsController
layout 'lightbox'
def update
account_update_params = devise_parameter_sanitizer.sanitize(:account_update)
# required for settings form to submit when password is left blank
if account_update_params[:password].blank?
account_update_params.delete("password")
account_update_params.delete("password_confirmation")
end
#user = User.find(current_user.id)
if #user.update(account_update_params) # Rails 4 .update introduced with same effect as .update_attributes
set_flash_message :notice, :updated
# Sign in the user bypassing validation in case his password changed
sign_in #user, :bypass => true
redirect_to after_update_path_for(#user)
else
render "edit"
end
end
# for Rails 4 Strong Parameters
def resource_params
params.require(:user).permit(:email, :password, :password_confirmation, :current_password, :user_country)
end
private :resource_params
protected
def after_sign_up_path_for(resource)
resource.update(user_country: set_location_by_ip_lookup.country) #use concerns/CountrySetter loaded by ApplicationController
root_path
end
end
If it helps, this is how I find the country of a visitor:
module CountrySetter
extend ActiveSupport::Concern
included do
before_filter :set_location_by_ip_lookup
end
# we use geocoder gem
# output: ip address
def set_location_by_ip_lookup
if Rails.env.development? or Rails.env.test?
Geocoder.search(request.remote_ip).first
else
request.location
end
end
end
you need to use doubles in your test.
describe UserController do
it 'sets the request location with Geocoder' do
geocoder = class_double("Geocoder") # class double
location = double('location', country: 'some country') # basic double with return values
expect(geocoder).to receive(:search).and_return(location) # stubbing return value
post :create, user_params
user = assigns(:user)
expect(user.user_country).to eq('some country')
end
end
this way, we create a fake location and geocoder, and also create fake return values. then we assert that the return value from geocoder gets persisted as a user attribute. this way, we don't actually need to test whether Geocoder is working, and simplifies our test setup a lot.
the concepts used here:
https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/basics/test-doubles
https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/verifying-doubles/using-a-class-double
https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/configuring-responses/returning-a-value
if you want to be more robust, you might be able to fake ip address in the test request context, then assert what arguments the fake geocoder is receiving with argument matching: https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/setting-constraints/matching-arguments
in general, you should utilize mocks to isolate your tests' concerns. see https://relishapp.com/rspec/rspec-mocks/v/3-0/docs for more documentation
I want to validate if the phone number and email is unique, which i figured but how do i do if phone number or email is not unique send to user registration path?
I tried
validates :phone_number, :email, if uniqueness: false {redirect_to user_registration_path}
The model is by definition unaware of the request context. You cannot perform a redirect from the model.
In order to accomplish the task, in your controller validate the instance and if invalid perform the redirect.
Here's a pseudo-code:
class Controller
def action
# instance is your instance
if instance.valid?
# do what you need to do
else
redirect_to user_registration_path
end
end
end
You'll need to check this in your controller, since your model has nothing to do with redirects.
Your action could look like this:
class UsersController
def some_action
if User.where(params[:user].slice(:phone_number, :email)).exists?
redirect_to user_registration_path
else
# no redirect
end
end
end
This explicitly checks if another user with the phone number and email already exists, rather than validating the instance.
With the recent upgrade to Rails 4, updating attributes using code resembling the below does not work, I get a ActiveModel::ForbiddenAttributes error:
#user.update_attributes(params[:user], :as => :admin)
Where User has the following attr_accessible line in the model:
attr_accessible :role_ids, :as =>admin
# or any attribute other than :role_ids contained within :user
How do you accomplish the same task in Rails 4?
Rails 4 now has features from the strong_parameters gem built in by default.
One no longer has to make calls :as => :admin, nor do you need the attr_accessible :user_attribute, :as => admin in your model. The reason for this is that, by default, rails apps now have 'security' for every attribute on models. You have to permit the attribute you want to access / modify.
All you need to do now is call permit during update_attributes:
#user.update_attributes(params[:user], permit[:user_attribute])
or, to be more precise:
#user.update_attributes(params[:user].permit(:role_ids))
This single line, however, allows any user to modify the permitted role. You have to remember to only allow access to this action by an administrator or any other desired role through another filter such as the following:
authorize! :update, #user, :message => 'Not authorized as an administrator.'
. . . which would work if you're using Devise and CanCan for authentication and authorization.
If you create a new Rails 4 site you'll notice that generated controllers now include a private method which you use to receive your sanitised params. This is a nice idiom, and looks something like this:
private
def user_params
params.require(:user).permit(:username, :email, :password)
end
The old way of allowing mass assignment was to use something like:
attr_accessible :username, :email, :password
on your model to mark certain parameters as accessible.
Upgrading
To upgrade you have several options. Your best solution would be to refactor your controllers with a params method. This might be more work than you have time for right now though.
Protected_attributes gem
The alternative would be to use the protected_attributes gem which reinstates the attr_accessible method. This makes for a slightly smoother upgrade path with one major caveat.
Major Caveat
In Rails 3 any model without an attr_accessible call allowed all attributes though.
In Rails 4 with the protected_attributes gem this behaviour is reversed. Any model without an attr_accessible call has all attributes restricted. You must now declare attr_accessible on all your models. This means, if you haven't been using attr_accessible, you'll need to add this to all your models, which may be as much work as just creating a params method.
https://github.com/rails/protected_attributes
This problem might also be caused by the Cancan gem
Just add to application_controller.rb
before_filter do
resource = controller_name.singularize.to_sym
method = "#{resource}_params"
params[resource] &&= send(method) if respond_to?(method, true)
end
Works without any further modifications of code
got it from here: https://github.com/ryanb/cancan/issues/835#issuecomment-18663815
Don't forget to add your new user_params method to the controller action:
def create
#user = User.new(user_params)
#user.save
redirect_to 'wherever'
end
def create
#user = User.create(user_params)
....
end
def update
#user = User.find(params[:id])
if #user.update_attributes(blog_params)
redirect_to home_path, notice: "Your profile has been successfully updated."
else
render action: "edit"
end
end
private
def user_params
params.require(:user).permit(:name, :age, :others)
end