Best way to save attribute inside save block? - ruby-on-rails

In my controllers I often have functionality like this:
#account = Account.new(account_params)
if #account.save
if #account.guest?
...
else
AccountMailer.activation(#account).deliver_later
#account.update_column(:activation_sent_at, Time.zone.now)
flash[:success] = "We've sent you an email."
redirect_to root_path
end
end
What's the best way to send an email and update the activation_sent_at attribute without having to save the record twice? Calling update_column doesn't feel right to me here because AFAIK it creates an extra SQL query (correct me if I'm wrong).

Your code is fine. I wouldn't change it.
For example, you might be tempted to do something like this:
#account = Account.new(account_params)
#account.activation_sent_at = Time.zone.now unless #account.guest?
if #account.save
if #account.guest?
...
else
AccountMailer.activation(#account).deliver_later
flash[:success] = "We've sent you an email."
redirect_to root_path
end
end
Aside from the small issue that there's now repeated logic around #account.guest?, what happens if AccountMailer.activation(#account).deliver_later fails? (When I say "fails", I mean - for example - AccountMailer has been renamed, so the controller returns a 500 error.)
In that case, you'd end up with a bunch of account records which have an activation_sent_at but were never sent an email; and you'd have no easy way to distinguish them.
Therefore, this code warrants running two database calls anyway: One to create the record, and then another to confirm that an email was sent. If you refactor the code to only perform a single database call, then you'll become vulnerable to either:
Sending an email to a non-created user, or
Marking a user with activation_sent_at despite o email being sent.
The controller should be doing two transactions, not one. Which is why I said: Don't change it.

Related

Active Record Query - Rails

Currently I have a method called Visit and it basically adds on the the visit counter on Subscriber. This method gets triggered when a Subscriber enters their phone_number in a form. I simply want to make a active record query that will look something like this Susbcriber.find_by(params[id]).last.visit(this doesn't work BTW). Hopefully you get what I'm trying to do there, basically call the person that checked in last with their phone number. I'll show my code for clarity.
CONTROLLER METHOD:
def search
#subscriber = Subscriber.new
end
def visit
#subscriber = Subscriber.find_by(params[:phone_number])
if #subscriber
#subscriber.visit ||= 0
#subscriber.visit += 1
#subscriber.save
flash[:notice] = flash[:notice] = "Thank You #{#subscriber.first_name}. You have #{#subscriber.days_till_expired} until renewal"
redirect_to subscribers_search_path(:subscriber)
else
render "search"
end
end
There you can see the method that gets triggered when a Subscriber inputs their phone number. I simply want to call that person in the console.
CONTROLLER METHOD THAT IS NOT WORKING CURRENTLY:
def create
#subscriber = Subscriber.find(params[:subscriber_id])
#comment = #subscriber.comments.build(comments_params)
if #comment.save
flash[:notice] = "Thank you!"
redirect_to subscribers_search_path(:comments)
else
render "new"
end
end
This is the method that needs the new query to find the Subscriber that just entered their phone number.
Let me know if you need more info? Thank You!
I'm editing my answer to be more inclusive based on the comments. It seems like you've got a number of issues going on here and maybe you need to step back and rebuild this step by step. Consider test driving it and/or at least verify that you're getting what you expect using a debugging tool as you go along (byebug, pry, or even just judicious use of 'puts' and 'inspect').
The find_by method requires that you specify the attribute that you're trying to use to find the row by, as well as the value of the attribute
#subscriber = Subscriber.find_by(phone_number: params[:phone_number])
If you are using the model's primary key, just use find:
#subscriber = Subscriber.find(params[:id])
If you're calling a controller action, params should always be present. Try inspecting the params before moving on with any of the rest of the code. Make sure that you're getting what you expect. If not, evaluate your view code.

Use devise_invitable to send invitation upon creating new model record

Non-technical Context
I'm working with an app that manages data for different companies. For the purposes of this situation I have two models User and Company. Often we set up a company for the user before they even have an account, we then create the user and give the login info to the appropriate party via some clunky means like a phone call. We would like to send an invitation to the user upon creating a new company.
Technical Info
We are using devise for our user management and recently came across a great little gem called devise_invitable. Which sends out invitations through devise's mailer.
The Problem
While devise_invitable works really well for simply inviting a user, I can't figure out how to get it to send out an invitation using a controller (like the companies_controller) other than the invitations_controller. My key hangup is that the invitable functionality seems to require us to either go through invitations_controller and its respective views or use its User.invite! method. Both creates a user and sends an invitation. This means that if I add User.invite! to the create method as shown below, I'll duplicate the user.
My Question
How can I both create a new company and invite a new user all within the companies_controller? I've been creating users (just not inviting them) with the following methods. If anyone knows how I should alter them to use the invitation functionality, I would really appreciate it.
From companies_controller
def new
#company=Company.new
#user=#company.user_companies.build.build_user
#folder=#company.folders.build
#stock=#company.stocks.build
#stock.security_class="Common"
#stock.security_series=""
end
def create
#company = Company.new(company_params)
if #company.save
redirect_to users_admin_path(#user), notice: "User successfuly created!"
else
redirect_to welcome_index_path
end
end
Anyway, thanks for any ideas!
Alright, figured it out (or at least figured one way out). Basically I created a set of dummy variables in the Company model like so:
attr_accessor :user_email, :user_fname, :user_lname
Then I updated the controller as follows
def new
#company=Company.new
#folder=#company.folders.build
#stock=#company.stocks.build
#stock.security_class="Common"
#stock.security_series=""
end
def create
#company = Company.new(company_params)
if #company.save
invitedUser=User.invite!(email: company_params[:user_email], fname: company_params[:user_fname], lname: company_params[:user_lname], invited_by: current_user)
if invitedUser.save
UserCompany.create(user: invitedUser, company: #company, company_role: "Owner")
redirect_to companies_path, notice: "User successfuly created!"
end
else
redirect_to welcome_index_path
end
end
The key highlights were
to remove #user=#company.user_companies.build.build_user because I don't want to build the user, I want it to be created by the invite! method.
To use User.invite! to create the user and grab the necessary data out of the submitted form's parameters
To manually add a join record (because I have a many-to-many relation between Company and User).
That's it really.

Is it safe to just write #user.save in controllers

Suppose i have a model User and a controller UsersController,
in my create actions, i can write
def create
#user = User.new(user_params)
#user.save
redirect_to root_path
end
or
#user = User.new(uer_params)
if #user.save
redirect_to users_path
else
render :new
end
Replicate above 2 actions for Update and destroy also
My question is related to 2nd create action,
Is is necessary to add if else end. what worse could happen i just have create actions like 1st one.
Note: Please ignore the validations part for now.
Just suppose I do not any validations.
What are the other possible conditions in which create/update/destroy will fail apart from validations and which one is the good practice.
Given that you don't want to perform any validations or any checks on the status of the save, then there's no reason for the conditional. In fact, in that case there's also no reason for the #user instance variable. This is all you would need:
def create
User.create(user_params)
redirect_to root_path
end
The conditional is just to do different things based on the status of the save. The instance variable is only to pass the User object to the view. But if you're always doing a redirect then you can't utilize the instance variable anyway, so no need.
What's "right" here is up to the needs of your application. Do the minimum necessary until you have a problem and then fix it.
This:
if User.create(user_params)
is always true. create returns on active reocrd object regardless whether it was successfully created or not. This is why we usually do:
#user = User.new(uer_params)
if #user.save
redirect_to users_path
else
render :new
end
Also note that we are ot redirecting to a new action. The reason is that we already has an #user variable, which 1) holds all the attributes entered by user 2) has all the validation errors attached to it. All we need to do is to render :new template and let Rails do its magic.
Note: If we ignore the validation, then there is no difference which option you will use. You don't need if/else statement neither as it will throw an exception if save fails for any other reason than validation (unless you have after/before_save hooks).
Difference between create & save ?
From the docs:
create
Tries to create a new record with the same scoped attributes defined
in the relation. Returns the initialized object if validation fails
save
.... By default, save always run validations. If any of them fail the
action is cancelled and save returns false. However, if you supply
validate: false, validations are bypassed altogether.
What about validations?
Well,
Create will try saving and returns the initialized object anyway (successful or failed save after validations)
Save will try saving and returns true for successful save and false otherwise
Note that you can skip validations by passing false to save
#user.save(false)
So, what about Conditions?
If you chose to skip validations, using Create or Save(false) then you don't need conditions, while if you need validations then you probably need to check how things went then give user some feedback, hence the conditions

Rails validation is still firing despite unless option evaluating to true

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

Rails: How to enter value A in Model X only if value A exists in Model Y?

I'm trying to build a registration module where user can only register if their e-mail is already in an existing database.
Models:
User
OldUser
The condition on User will be
if OldUser.find_by_email(params[:UserName]) exists, allow user registration.
If not, then indicate error message.
This is really simple to do in PHP where I can just run a function to execute a mysql query. However, I couldn't figure out how to do it on Rails. It looks like I have to create a custom validator function but seems to be overkilled for a such simple condition.
It should be pretty simple to do. What have I missed?
Any pointer?
Edit 1:
This solution by dku.rajkumar works with a slight modification:
validate :check_email_existence
def check_email_existence
errors.add(:base, "Your email does not exist in our database") if OldUser.find_by_email(self.UserName).nil?
end
For cases like this, is it better to do validation in the model or at the controller?
you can do it as
if OldUser.find_by_email(params[:UserName])
User.create(params) // something like this i guess
else
flash[:error] = "Your email id does not exist in our database."
redirect_to appropriate_url
end
UPDATE: validation in model, so the validation will be done while calling User.create
class User < ActiveRecord::Base
validates :check_mail_id_presence
// other code
// other code
private
def check_mail_id_presence
errors.add("Your email id does not exist in our database.") if OldUser.find_by_email(self.UserName).nil?
end
end
I'd recommend starting with Devise.
See https://github.com/plataformatec/devise
Even if you have unusual needs like these, you can normally adapt it. Once you get to know it, it's extremely powerful, solid and debugged, and you can do all sorts of things with it.
Bellow is just an initial implementation .../app/controller/UsersController for User registration related actions.
def new
#user = User.new
end
def create
#user = User.new(params[:user])
#old_user = User.find_by_email(user.email)
if #old_user
if #user.save
# Handle successful save
else
render 'new' # and render some error message telling why registration was not succeed
end
else
# render some page with some sort of error message of 'new' new users
end
end
Update:
Check out the following resources for more info:
Ruby on Rails Tutorial
Rails: User/Password Authentication from Scratch, Part I/II

Resources