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
Related
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.
In every example for create or update action that I see, they have something like this.
def create
#user = User.new(params)
if #user.save
redirect_to #user
else
render 'new'
end
end
Here how the redirect_to #user goes to show action of the controller. Can anybody explain me this?
Let's start from the redirect_to documentation.
redirect_to post_url(#post)
is used to redirect to a specific URL generated using one of the Rails route helpers. In your case, it means you can write
redirect_to user_url(#user)
However, redirect_to also accepts a single model instance. Behind the scenes, redirect_to relies on url_for to generate an URL from the input when the input is not an object.
url_for, in turns, when you pass an instance of a model by default will compute the corresponding GET action to view the model.
In conclusion, the following code:
redirect_to #post
is equivalent to
redirect_to post_url(#post)
However, personally I prefer the explicit version. Even if it's a little bit longer, I've noticed it tends to produce more maintainable code in the long run. Writing the full route will allow you to easily search your code base when you need to debug or rename routes.
It's all in the documentation.
Record - The URL will be generated by calling url_for with the options, which will reference a named URL for that record.
So, url_for will be called on your #user which will produce the url for redirection. (/users/1234 or something)
This is just one of many ways to do redirection, by the way.
In Ruby (the language which supports Rails), you set #instance_variables to store data for that request. Whilst you can store many types of data in a variable, Rails often assigns #model objects to them...
#user = User.find 1
#-> #user = <User id: "1", name: "john" .... >
This means that whenever you use a helper (such as redirect_to, or even a path_helper), you're actually able to pass the object to it and Rails will extract the data it requires.
For example...
user_path(#user)
edit_user_path(#user)
In the instance of a path, the helper extracts the id of the object; redirect_to extrapolates the functionality to route the request to the show path for that user.
Passing redirect_to accepts an object, invoking the show action for that object.
The reason why this is important is to understand that Ruby (& by virtue Rails) is object orientated.
Object orientated programming means that you should be dealing with objects (not variables).
In the case of Rails, each model should be an object. Every time you load the model, or create a new instance of it, you should be dealing with the object rather than the data.
Therefore, allowing you to pass #objects to methods such as redirect_to is just another way to make Rails more object-orientated.
Accoding to this stackoverflow post, I can use the following to detect attribute changes:
self.changes
And it does work when I use it in the model, such in an after_update callback:
def check_activity
changes = self.changes
puts changes
end
Unfortunately, it is not working in my controller update action. I tried to use it before the task was saved and after the task was saved:
def update
changes = #task.changes
if #task.update_attributes(task_params)
changes2 = #task.changes
flash[:notice] = "Successfully updated task."
redirect_to polymorphic_path([#taskable, #task])
else
render :edit
end
end
Unfortunately in both cases, it is empty:
Empty ActiveSupport::HashWithIndifferentAccess
What might I be doing wrong?
There aren't any changes to show after you've made the change (IE update_attributes runs)
You could try using assign_attributes instead of update_attributes, checking that everything is valid (object.valid?) getting your changes and then saving
Just check out ActiveModel::Dirty
Changes method does not returns changes after save.you should use it in before save, if you want to check the value after save use these ones previous_changes etc. for detail checkout http://api.rubyonrails.org/classes/ActiveModel/Dirty.html.
I have an ActiveRecord model named Document and have implemented CRUD operations around it. I just have a problem with persisting a Document instance between requests when validation fails (be cause I wanna redirect to another page when this happens).
First, I tried storing the instance in the flash session:
# documents_controller.rb
def new
#document = flash[:document] || Document.new
end
def create
document = Document.new(document_params)
if document.save
return redirect_to documents_path
end
flash[:document] = document
redirect_to new_document_path
end
With the code above, I was expecting that the actual Document instance was stored in the flash session, but instead it became a string which looks somewhat like #<Document:0xad32368>. After searching online for a while, I found out that for some reasons you cannot store ActiveRecord objects in sessions.
There are a lot of suggestions about just storing the object's id in the flash session, but I can't do that because as you can see, the object is not yet stored in the database.
Next, I tried reconstructing the Document instance after the redirect, taking advantage of the instance's attributes method (which returns a serializeable hash that can be stored in the session):
# documents_controller.rb
def new
#document = Document.new(flash[:document_hash] || {})
end
def create
...
flash[:document_attributes] = document.attributes
redirect_to new_document_path
end
This almost solved the problem, except for the part in which the validation errors (document.errors) are not preserved. Also, if this is used to persist an instance already stored in the database (in the case of failed validations when updating a Document instance), I'm not sure which between the original attributes and the new attributes will get persisted.
Right now I've already run out ideas to try. Anyone who has a decent solution for this?
EDIT:
You might be wondering why I still have to redirect to another page instead of just rendering the new document view template or the new action in the create method. I did so because there are some things in my views that are dependent on the current controller method. For example, I have a tab which needs to be highlighted when you are on the document creation page (done by checking if action_name == "new" and controller_name == "documents"). If I do:
def create
...
render action: "new"
end
the tab will not get highlighted because action_name will now be create. I also can't just add additional condition to highlight the tab if action_name == "create" because documents can also be created from the the index page (documents_path). Documents can also be updated from the index page (documents_path) or from the detail page (document_path(document)), and if validation fails in the update method, I'd like to redirect to the previous page.
If I really need to fake persisting something between requests (all of the variables that you set are lost between requests), I will ususally put the relevant attributes into hidden fields in the new form.
In your case, this is overkill. In your code, you are redirecting, which causes a new request:
def create
document = Document.new(document_params)
if document.save
return redirect_to documents_path
end
flash[:document] = document
redirect_to new_document_path
end
You can easily render the output of another action, instead of redirecting, by using render action: 'action_to_render'. So in your example, this would probably be:
def create
#document = Document.new(document_params)
if #document.save
render action: 'index'
else
render action: 'new'
end
end
Which can be simplified to:
def create
#document = Document.new(document_params)
action_to_render = #document.save ? 'index' : 'new'
render action_to_render
end
If you need extra logic from the action, you can refactor the logic to a method called from both actions, or simply call the other action from the current one.
It is fine once in a while, but I would caution that having to jerk around with the rendering too much is usually indicative of poor architecture.
Edit:
An additional option, given the newly highlighted constraints, could be to make the new and create methods the same. Remove the new action and routes, and make create answer for GET and PATCH requests. The action might look something like:
def create
#document = Document.new(document_params)
request.patch? && #document.save && redirect_to( documents_path )
end
I actually use something very similar to this for almost all of my controllers, as it tends to DRY things significantly (as you can remove the extra probably identical view, as well)
Another option would be to just use an instance variable to keep track of the active tab in this instance, and make the rest of the code a lot cleaner.
SOLVED
I was able to make a workaround for it using ActiveSupport::Cache::Store (as suggested by #AntiFun). First I created a fake_flash method which acts closely like the flash sessions except that it uses the cache to store the data, and it looks like this:
def fake_flash(key, value)
if value
Rails.cache.write key, value
else
object = Rails.cache.read key
Rails.cache.delete key
object
end
end
And then I just used it like the flash session.
# documents_controller.rb
def new
...
#document = fake_flash[:document] || Document.new
...
end
def create
document = Document.new document_params
...
# if validation fails
fake_flash :document, document
redirect_to new_document_page
end
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