I have a User model with three different types fields which I would like to be able to update independently from each other
The same page has 3 different forms which act on the same model:
change the user profile image
change the user name / email
change the user password
The reason they are separated:
photo's automatically get uploaded when they are selected (without requiring a password),
name / email can be changed without requiring a password but require submitting a form,
password requires the current password to change it.
Currently in User#update I have a series of if/else branches for logic: if params[:commit] == "Update Password" I update the password, elsif params[:commit] == "Update Info" I update their name / email, etc.
I don't like how long the logic gets, and I don't think its good practice to tie the controller logic into the view (since the logic is based off of params[:commit] text that appears on the submit buttons).
Is there a better way to do this?
To get rid of if..elsif..elsif chain you can split your update action into update_password, update_info, etc and set your form actions accordingly. Of course you will need to update your routes also.
In your controller could you check to see which parameters have been sent and then act appropriately?
if params[:password] && params[:password_confirmation]
#user.password = params[:password]
end
if params[:email]
#user.email = params[:email]
end
if #user.save
etc...
Then you have one route that behaves as expected dependent on what is sent.
Related
How to send a variable/parameter from an action without using the URL?
I need to create a user in multiple steps, and the user is created on the 2nd step. I need to receive the email of the user from the first step.
One first step, I receive the email in params, and use this line of code: redirect_to new_user_registration_path(email: params[:email]) to send it out to the next page/action.
For some reasons, I have been told that I can't use emails in URLs, so now I need to send the email under the hood which surely is possible through the POST method, but redirect_to doesn't support POSTs requests.
There could be a suggestion of using render instead of redirect_to, but I'm using Devise, so I would like to hand over the functionality to Devise, instead of manually doing it all by myself.
There is another idea of using cookies to store the email address, but I'm interested in more of a Rails way.
There can be another way too, one way is to using session
On the first step of form submission store email in session variable and use it on the further step and after that clear that session variable.
Eg -
def first_step
#user = User.new
end
def second_step
# Assuming after first step form is being submitted to second step
session[:email] = params[:email]
end
def next_step
user_email = session[:email]
end
Hereby session[:email] will be available everywhere except model layer unless it is set to blank (session[:email] = nil), that should be set to blank after the user is created.
You can use the flash for this
flash[:email] = params[:email]
redirect_to new_user_registration_path
in your view, something like this
<%= hidden_field_tag :email, flash[:email]
You will need to add this line
class ApplicationController < ActionController::Base
add_flash_types :email
I'm just posting this as a possible solution, I realize this is not some best-practice solution for every case
In the Rails Tutorial Chapter 8 it is tested that the 'remember me' checkbox is working.
In a real contest, if a user checks the 'remember me' checkbox in the login page, after login the create action of the sessions controller uses the remember(user) helper, which creates a remember_token for the user and update the remember_digest attribute in the user model (via the remember method in user.rb), then sets cookies[:user_id] = user.id and cookies[:remember_token] = user.remember_token.
The book says that in the test "Ideally, we would check that the cookie’s value is equal to the user’s remember token, but as currently designed there’s no way for the test to get access to it: the user variable in the controller has a remember token attribute, but (because remember_token is virtual) the #user variable in the test doesn’t".
The test is defined below:
def setup
#user = users(:michael)
end
test "login with remembering" do
log_in_as(#user, remember_me: '1')
assert_not_nil cookies['remember_token']
end
First of all, considering how cookies['remember_token'] is defined (cookies[:remember_token] = user.remember_token), if it is true that we cannot access the remember token attribute, I wonder how can we check that cookies['remember_token'] is not nil.
Although the fixtures do not define any remember token for user michael, the log_in_as test helper method is defined to post to the login_path correct values for params[:session], so I am wondering: aren't these values taken by the create action of the sessions controller? If this is the case, then the create action should do the same job as described above for the real contest: the remember(user) helper would create a remember_token for the user and we could check if cookies['remember_token'] = user.remember_token.
I do not understand why we should not be able to access user.remember_token.
#user is created through a fixture. The issue is, #remember_token is a virtual attribute, which means it does not map to a database column. It gets set only when the #remember function is called on a User instance, and when that instance dies, it dies with it (though a digest of it is saved in the database, and an encrypted version of it is saved in the user's cookies).
What you are doing there using the #log_in_as is that, first you create a User, and then in the #log_in_as you take that user's email address and password, and send it to the controller. The controller finds that user from the database using the email that you provided, and goes ahead and calls the #remember function on that instance.
As you can see, the #remember function has never been called on your #user instance, so it never received the remember_token. But through that controller action, a cookie is set and a digest is saved in the database. So what you test there is to check if the cookie is set or not.
If you want to be more meticulous, I guess another thing you can do is to check if the digest of the cookie is the same as the digest in the database.
I want my users to be able to update their email addresses, if they authenticate themselves.
My code looks something like this...
#user = User.find_by_email(params[:user][:old_email].downcase)
if #user
if #user.authenticate(params[:user][:password])
#user.email = params[:user][:email]
if #user.update_attributes(email: params[:user][:email])
Something along those lines. Basically the params are the old_email, the email which the user wants to change to, and their password. We pull the record that matches their old_email (current email). Then we verify the password matches. If so, we want to change the email and the email only, IF of course the email meets the criteria in our model for the :email column.
The problem I'm having is, when I get to #user.update_attributes(em... I get the following error
"Password is too short (minimum is 6 characters)"
I don't want it to verify on password, password should not even be involved in the update whatsoever. I'm only interested in the changing the email here. I want validation only on the email field, and none of the others, and I only want to update the email field. How can I achieve this?
You can update specific column by #update_columns. It updates the attributes directly in the database issuing an UPDATE SQL statement and sets them in the receiver.
It will skip validations and callbacks.
#user.update_columns(email: params[:user][:email])
Remember to sanitise the email address before updating.
Validation belongs in the model but as you need to validate in controller, define a private action:
private
def valid_email?(email)
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
email.present? && (email =~ VALID_EMAIL_REGEX)
end
Then your condition check would be:
if #user.authenticate(params[:user][:password]) && valid_email?(params[:user][:email])
I have a form_for established to edit a resource (here it is a user). In the model, it is specified that some attributes cannot be blank (e.g. password). I have a second form to edit this user as an admin. This form does not require the user password but can be filled to change this user's password. The problem is that the validation fails cause no password is specified (Validation fail: Password cannot be blank).
I'd like to know if there is a way to edit the resource this way, without deleting the password field from parameters when it's blank.
#user.update!(user_params)
You can do something like a param deletetion. In your update method before the save do something like.
if params["user"].has_key? "password"
if params["user"]["password"].empty? and user_is_admin?
params["user"].delete("password")
end
end
Replace user_is_admin? with your own admin checking method.
I use devise_invitable in my app to allow users to send invitations. I realized a bad case in which a user has been invited but ignores the invitation and later returns to the app to sign up on their own. Because devise_invitable handles invitations by creating a new user using the provided email address for the invitation, my uniqueness validation on the email field will cause Rails to complain, telling the user that the email address is already taken.
I'm trying to write some logic to handle this case. I see two paths - either figure a way to detect this and destroy the previously created user and allow the new one to be created, or detect the user was invited and execute another flow. I've decided to implement the second option, as I'd like to still utilize the invitation if possible.
My limited experience has me questioning if what I've written will work, but I can't actually fully test it because the Rails validation on the email is triggered. I've made sure Devise's :validatable module is inactive. I created a method that (I think) will detect if a user was invited and in that case the uniqueness validation should be skipped.
#user.rb
...
validates :email, uniqueness: true, unless: :was_invited?
...
def was_invited?
if self.invitation_sent_at.present? && self.sign_in_count == 0
true
else
false
end
end
FWIW, I had originally written this in shorthand rather than breaking out the if/else, but I wanted to be very explicit in an effort to find the bug/failure.
The hope is that once the form passes validation, the create action will do some detection about a user's invitation status and, if they were invited, redirect them to the accept_user_invitation_path. Again, I haven't been able to actually test this yet because I can't get around the validations.
#registrations_controller.rb
def create
if User.find_by_email(params[:email])
#existing_user = User.find_by_email(params[:email])
#existing_user.save(validate: false)
if #existing_user.was_invited?
redirect_to accept_user_invitation_path(:invitation_token => #existing_user.invitation_token)
end
else
super
end
end
In a desperate effort, you'll see I've also added the .save(validate: false) to try to short circuit it there, but it's not even getting that far.
If I comment out the email validation entirely, simply to test the rest of the logic/flow, I get a PG error complaining on uniqueness because of an index on the email address - I don't want to tear all this apart simply to test this method.
I've tried to mess with this for hours and I'm at a loss - any help is appreciated. Let me know if there's any other code you want to see.
Looking at the redirect:
redirect_to accept_user_invitation_path(:invitation_token => #existing_user.invitation_token)
I can see that there is no return which should mean that if that redirect was being called you should be getting an AbstractController::DoubleRenderError error as the parent controller's create method should be trying to render the new view.
From this I would guess that the query you are using to find the existing user is not actually returning a result, possibly because you are using params[:email] whereas if you are using the default views or a properly formatted form it should be params[:user][:email].
Maybe you should give more responsibilities to your controller...
If you find the user, use that, else create a new one. Assuming your form appears with http://yourapp/users/new, change it in your routes to http://yourapp/users/new/:email, making the user input their email before advancing to the form.
def new
#existing_user = User.find_by_email("#{params[:email]}.#{params[:format]}") || User.new
if #existing_user.was_invited? # will only work for existing user
redirect_to accept_user_invitation_path(:invitation_token => #existing_user.invitation_token)
else
render 'new'
end
end
def create
# do maybe something before saving
if #existing_user.save(user_params)
# do your magic
else
render 'new', notice: "Oops, I didn't save"
end
end