Proper user of strong parameters, assignments, and validators in Rails 4? - ruby-on-rails

What is the proper way to set strong parameters in a Rails controller and using validators with them? I've seen several examples of doing this several different ways.
This is what a typical controller def looks like for me:
# User model
class User < ActiveRecord::Base
validates :first_name, length: { in: 2..50 }, format: { with: /[a-zA-Z0-9\s\-\']*/ }
validates :last_name, length: { in: 2..50 }, format: { with: /[a-zA-Z0-9\s\-\']*/ }
validates :email, presence: true, length: { 5..100 }, format: { with: /**email regex**/ }, uniqueness: { case_sensitive: false }
end
# Controller def
def valid_email
# Strong parameters defined here? When is an error thrown for unmatched requires/permits?
params.require(:user)
params[:user].permit(:email)
# How to prevent blank params[:user][:email] from making unnecessary database call if it's blank?
#user = User.find_by(email: params[:user][:email])
unless #user.nil?
# Should work because in permit whitelist?
#user.assign_attributes(email: params[:user][:email])
# Should not work because not in permit whitelist?
#user.assign_attributes(first_name: params[:user][:first_name])
# Should work just sending to private def
#user.assign_attributes(savable_params)
# Validate entire model
if #user.valid?
#user.save
end
end
rescue => e
log_error(e)
render text: "Something bad happened. Contact support.", status: :unprocessed_entity
end
private def savable_params
params.require(:user).permit(:email)
end
How I understand it, is the params.require and params.permit allow for whitelisting of data passed to the controller/action, but doesn't keep "other" data from being seen in the params list (if it's passed).
So, leads into a few questions:
Proper way of whitelisting in a similar scenario a top?
How would you, say for instance, make sure params[:user][:email] isn't blank ("") before trying to do a User.find_by since we know that a blank email isn't valid anyway and do not want to make an unnecessary database call?
User.valid? will validate the entire model, even though I'm interested in only the :email attribute be valid before doing a User.find_by?
If you have the uniqueness validator on email, for instance, how would you keep uniqueness from hitting the database, if params[:user][:email] is blank and we're only interested in that email isn't blank, fits within a certain length, and matches the format?
Thanks again.

So this is updating an existing user?
Just do - assuming for some reason you are not sending a user id as part of your submission:
not_found if params[:user].blank? or params[:user][:email].blank?
#user = User.find_by email: params[:user][:email] or not_found
#user.update savable_params
if #user.valid?
flash.notice = "All good"
redirect_to #user
else
# this has not been saved
flash.now.alert = #user.errors.full_messages.to_sentence
render :(whatever you call to display the form in the first place
end
and add to your application_controller:
def not_found
raise ActionController::RoutingError.new('Not Found')
end
to your questions:
as per above. Validators run on your model, they have the logic, keep your controllers simple.
I added a line for that - should work, but normally you're sending up and id of the user, so it's not something I've done. I don't bother testing it.
The validation happens on the update,. it won't hit the db unless it passes, it's a few cycles, not something I've ever worried about. You could put another pre-validation in there, to save on the db roundtrip, but if your going to do that - why not put it on the html field, and save a server round trip.
Put uniqueness constraint on the model and the db (the model should get it, but for race conditions, the db is your backstop). Nothing will hit the db unless it passes your validations first, so it should be rare.
oh, and I think you'll need:
params.require(:user).permit(:email, :first_name)
to get the other field to update.

Related

user.save returns false in console, but true when done via the UI

I'm a programming/rails beginner, and have encountered a bug I cannot wrap my head around.
I'm using/learning about the "has_secure_password" method. When I try and create a user in my console with a mismatched password/confirm_password, the console returns false and the error is "Password confirmation doesn't match Password". But, when I try and do the same thing within the UI given the below code (+ a view), it saves just fine! Now, notice that in my "user_params" method, I accidentally forgot to permit ":password_confirmation" which is how I noticed this issue in the first place. With that ":password_confirmation" added, the view throws an error but that's not the point. Why even without this is the new User record being successfully created with a mismatched password and password confirmation, even though it doesn't save in the console?
Here is my User model:
class User < ActiveRecord::Base
has_secure_password
validates :name, presence: true
validates :email, presence: true, format: /\A\S+#\S+\z/, uniqueness: {case_sensitive: false}
validates :password, length: {minimum: 4, allow_blank: true}
end
And my User controller:
class UsersController < ApplicationController
def index
#users = User.all
end
def show
#user = User.find(params[:id])
end
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
redirect_to #user, notice: "Thanks for signing up!"
else
render :new
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password)
end
end
This is happening because the password_confirmation attribute is optional. When it isn't supplied to the model that has_secure_password, the model simply accepts the password.
When your password confirmation attribute isn't whitelisted in your controller via user_params, it isn't being passed to the model at all, which is why mismatches appears not to throw an error. In truth the validation isn't taking place at all.
This works in your console because it creates a user without involving a controller or strong parameter whitelisting.

Updating Strong Params Only

First of all, I believe there must be some people, who already asked this question before but I don't know how can I google this problem. So, if it is duplicate I am sorry.
I am working on a social media site. I have user model, which I use to register users to the site. It validates, name, email, and password when registering.
I use the same model to make users edit their informations, like username.
This is what I have in my update controller:
def update
# Find an existing object using form parameters
#profile = User.find_by_id(current_user.id)
# Update the object
if #profile.update_attributes!(settings_profile_params)
# If save succeeds, redirect to itself
redirect_to request.referrer
else
# If save fails, redisplay the form so user can fix the problems
render('edit')
end
end
private # user_params is not an action, that is why it is private.
def settings_profile_params
params.require(:user).permit(:first_name, :last_name, :username, :school, :program, :website, :information)
end
The problem is, I only want to update strong parameters that I defined there. But I am getting an exception because of password validation. I don't know why am I getting this exception. How can I set up system to update the values in strong parameter only.
Thank you.
You can achieve this by changing you password validation. You need to add a condition on password validation.
# Password
validates :password,
:presence => {:message => 'Password cannot be blank'},
:length => {:within => 8..99, :message => 'Password length should be within 8 and 99 characters'}
:if => Proc.new { new_record? || !password.nil? }
By calling update_attributes you are implicitly invoking the same range of validations as an other update and save. You need to update on the specific params you're targeting (e.g. omitting :password).
Here, we can store that list of permitted keys in a variable that is reusable. Then we call update_attribute on each of those keys — doing so within a reduce that gives the same true/false for the switch to edit or display.
def update
# Find an existing object using form parameters
#profile = User.find_by_id(current_user.id)
# Update the object
if PERMITTED_KEYS.reduce(true) {|bool, key| bool &&= #profile.update_attribute(key, #profile.send(key)) }
# If save succeeds, redirect to itself
redirect_to request.referrer
else
# If save fails, redisplay the form so user can fix the problems
render('edit')
end
end
private
PERMITTED_KEYS = [:first_name, :last_name, :username, :school, :program, :website, :information]
# user_params is not an action, that is why it is private.
def settings_profile_params
params.require(:user).permit(PERMITTED_KEYS)
end
Having not used strong_parameters gem before, I think this would be more idiomatic to the use of the gem:
def update
# Find an existing object using form parameters
#profile = User.find_by_id(current_user.id)
# Update the object
if settings_profile_params.keys.reduce(true) {|bool, key| bool &&= #profile.update_attribute(key, #profile.send(key)) }
# If save succeeds, redirect to itself
redirect_to request.referrer
else
# If save fails, redisplay the form so user can fix the problems
render('edit')
end
end
private
# user_params is not an action, that is why it is private.
def settings_profile_params
params.require(:user).permit(
:first_name, :last_name, :username,
:school, :program,
:website, :information
)
end
Though, I still think this is a duplicate question, since it regard how to update model data without all of the defined validation. I've answered in case the update_attributes loop is felt to be a sufficiently unique solution to warrant non-duplication.
Okay, now I found the problem. First of all, #Muntasim figured out a way to solve this problem. But I actually don't need to use this solution, because there is another easy way to fix this.
In this situation, when I let users to update their profiles, rails should not validate my password or any other column in user model, if I don't ask it to. But why was it validating? Because I have validates :password in user model. Instead it has to be validates :digest_password. Because I am using bcrypt.
I don't know why :password was working fine when I register even though I used bcrypt.

Registering a user through Rails API and returning validation errors

I'm trying to build a controller action which will create a new user in my application. I want it to attempt to create the user and if it succeeds, return the User model and if it fails show the validation errors. So far I've got this mostly up and running:
# app/controllers/v1/users_controller.rb
def create
#user = User.create(user_params)
end
private
def user_params
params.require(:user).permit(:email, :password) if params[:user]
end
and I'm using JBuilder in my view like so:
# app/views/v1/users/create.json.jbuilder
if #user.errors
json.errors #user.errors
else
json.user #user
end
This looks like it'd work to me, however when I create a new user that doesn't have any validation errors I get this output rather than the User that just got created:
{"errors":{}}
How come it's printing out the empty errors when the user has no validation errors?
In Ruby, only nil and false evaluate to false in the if statement. In your example above, #user.errors is an ActiveModel::Errors class and therefore evaluate to true. Try changing the code above to:
if #user.errors.present?
json.errors #user.errors
else
json.user #user
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.

Force validation of blank passwords in Authlogic

I'm adding a password reset feature to my Rails application that uses Authlogic. I was following the guide here: http://www.binarylogic.com/2008/11/16/tutorial-reset-passwords-with-authlogic/ and everything works as I'd like except for one thing: the password reset form accepts blank passwords and simply doesn't change them.
I've been searching around, and have learned that this is the intended default behavior because it allows you to make user edit forms that only change the user's password if they enter a new one, and ignore it otherwise. But in this case, I specifically want to enforce validation of the password like when a user initially registers. I've found two possible solutions for this problem but haven't been able to figure out how to implement either of them.
1) Someone asked this same question on Google Groups:
User model saves with blank password
Ben's response was to use #user.validate_password = true to force validation of the password. I tried this but I get an undefined method error: undefined method 'validate_password_field=' for #<User>.
2) There seems to be an Authlogic configuration option called ignore_blank_passwords. It is documented here:
Module: Authlogic::ActsAsAuthentic::Password::Config#ignore_blank_passwords
This looks like it would work, but my understanding is that this is a global configuration option that you use in your initial acts_as_authentic call in the User model, and I don't want to change it application-wide, as I do have a regular edit form for users where I want blank passwords to be ignored by default.
Anyone found a solution to this? I see validate_password= in the change log for Authlogic 1.4.1 and nothing about it having been removed since then. Am I simply using it incorrectly? Is there a way to use ignore_blank_passwords on a per-request basis?
This is kind of an old thread, but since it is unanswered I'll post this.
I've managed to do it a bit more cleanly than the other solutions, "helping" authlogic validations with my own.
I added this to user:
class User < ActiveRecord::Base
...
attr_writer :password_required
validates_presence_of :password, :if => :password_required?
def password_required?
#password_required
end
...
end
You can reduce it to two lines by making an attr_accessor and using :if => :password_required (no interrogation), but I prefer this other syntax with the interrogation sign.
Then your controller action can be done like this:
def update
#user.password = params[:user][:password]
#user.password_confirmation = params[:user][: password_confirmation]
#user.password_required = true
if #user.save
flash[:notice] = "Password successfully updated"
redirect_to account_url
else
render :action => :edit
end
end
This will have a local effect; the rest of the application will not be affected (unless password_required is set to true in other places, that is).
I hope it helps.
This what I did.
class User < ActiveRecord::Base
attr_accessor :ignore_blank_passwords
# object level attribute overrides the config level
# attribute
def ignore_blank_passwords?
ignore_blank_passwords.nil? ? super : (ignore_blank_passwords == true)
end
end
Now in your controller, set the ignore_blank_passwords attribute to false.
user.ignore_blank_passwords = false
Here, you are working within the confines of AuthLogic. You don't have to change the validation logic.
User.ignore_blank_passwords = false
Use model, not object for setting this property.
def update_passwords
User.ignore_blank_passwords = false
if #user.update_attributes(params[:user])
...
end
User.ignore_blank_passwords = true
end
Maybe test the value of the parameter in the controller? (air code):
def update
#user.password = params[:user][:password]
#user.password_confirmation = params[:user][: password_confirmation]
if #user.password.blank?
flash[:error] = "Password cannot be blank"
render :action => :edit
return
end
if #user.save
flash[:notice] = "Password successfully updated"
redirect_to account_url
else
render :action => :edit
end
end
Apart from zetetic's solution you could do it this way:
def update
#user.password = params[:user][:password]
#user.password_confirmation = params[:user][: password_confirmation]
if #user.changed? && #user.save
flash[:notice] = "Password successfully updated"
redirect_to account_url
else
render :action => :edit
end
end
You're basically checking if authlogic changed the user record (which it doesn't if the password is empty). In the else block you can check if the password was blank and add an appropriate error message to the user record or display a flash message.

Resources