Ruby on Rails controller design - ruby-on-rails

When I look at examples of Rails controllers, I usually see something like this:
class WidgetController < ActionController::Base
def new
#widget = Widget.new
end
def create
#widget = Widget.new(params[:id])
if #widget.save
redirect_to #widget
else
render 'new'
end
end
end
This works, but there's a couple problems:
Routes
If I add widgets to my routes.rb file:
Example::Application.routes.draw do
resources :widgets
end
GET /widgets/new will route to new and POST /widgets will route to create.
If the user enters incorrect information on the new widget page and submits it, their browser will display a URL with /widgets, but the new template will be rendered. If the user bookmarks the page and returns later or refreshes the page, the index action will be called instead of the new action, which isn't what the user expects. If there's no index action or if the user doesn't have permission to view it, the response will be a 404.
Duplication of code
As a contrived example, let's say I had some tricky logic in my new method:
def new
#widget = Widget.new
do_something_tricky()
end
Using the current approach, I'd duplicate that logic in new and create. I could call new from create, but then I'd have to modify new to check if #widget is defined:
def new
#widget ||= Widget.new
do_something_tricky()
end
Plus, this feels wrong because it reduces the orthogonality of the controller actions.
What to do?
So what's the Rails way of resolving this problem? Should I redirect to new instead of rendering the new template? Should I call new inside of create? Should I just live with it? Is there a better way?

I don't think this is a problem in "the rails way" and there is no builtin functionality to allow this without getting your hands dirty. What does a user expects when bookmarking a form they just submitted and had errors? Users don't know better, and they shouldn't bookmark a failed form.
I think redirecting to new_widget_path is the cleanest solution. Yet, you should keep the errors and display them on the form. For this I recommend you keep the params in session (which I expect to be smaller than a serialized Widget object).
def new
#widget = widget_from_session || Widget.new
end
def widget_from_session
Widget.new(session.delete(:widget_params)) if session[:widget_params].present?
end
private :widget_from_session
# Before the redirect
session[:widget_params] = params
The code is self explanatory, Widget.new will only be called when widget_from_session returns nil, this is when session[:widget_params] is present. Calling delete on a hash will return de deleted value and delete it from the original hash.
UPDATE Option 2
What about submitting the form using ajax? Your controller could benefit from:
respond_to :html, :json
...
def create
#widget = Widget.new params[:widget]
#widget
respond_with #widget, location: nil
end
Based on the response code (which is set by Rails: 201 Created or 422 Unprocessable Entity), you could show the errors (available in the body of the response when validations fail) or redirect the user to #widget
This is how StackOverflow does it: https://stackoverflow.com/questions/ask. They submit the form asynchronously.

In general, I think the Rails way of solving the problem would be to put the tricky method onto the model or as a helper method, so the controller stays "thin" and you don't have to make sure to add custom behavior to both #new and #create.
EDIT: For further reading, I'd recommend the "Rails AntiPatterns" book, as they go through a lot of these common design issues and give potential solutions.

you put do_something_tricky() in its own method and call it inside the create action (but only when you're rendering the new template, ie when validation fails).
As for the bookmark issue, I don't know a good way to prevent that but to modify the routes and set the create action to the new action but using POST
get '/users/new' => 'users#new'
post '/users/new' => 'users#create'
UPDATE: using resources
resources :platos, except: :create do
post '/new' => 'plates#create', on: :collection, as: :create
end
then you can use create_platos_path in your forms

You don't need to write same function in two action , use before_filter instead.
If you want to have "widget_new_url" after incorrect submission then in your form add url of new widget path something like :url => widget_new_path .
Rails takes the url from Form .

I have this problem before, so I use edit action instead.
Here is my code.
Routes:
resources :wines do
collection do
get :create_wine, as: :create_wine
end
end
Controller:
def create_wine
#wine = Wine.find_uncomplete_or_create_without_validation(current_user)
redirect_to edit_wine_path(#wine)
end
def edit
#wine = Wine.find(params[:id])
end
def update
#wine = Wine.find(params[:id])
if #wine.update_attributes(params[:wine])
redirect_to #wine, notice: "#{#wine.name} updated"
else
render :edit
end
end
Model:
def self.find_uncomplete_or_create_without_validation(user)
wine = user.wines.uncomplete.first || self.create_without_validation(user)
end
def self.create_without_validation(user)
wine = user.wines.build
wine.save(validate: false)
wine
end
View:
= simple_form_for #wine, html: { class: 'form-horizontal' } do |f|
= f.input :complete, as: :hidden, input_html: { value: 'true' }
What I did is create a new action 'create_wine' with get action.
If user request 'create_wine', it will create a new wine without validation and redirect to edit action with a update form for attributes and a hidden field for compele .
If user has create before but gave up saving the wine it will return the last uncompleted wine.
Which means whether use save it or not, the url will be the same to /wines/:id.
Not really good for RESTful design, but solve my problem. If there is any better solution please let me know.

Related

(Rails) How to get 'id' out of edit url

I have a model called studies.
After action redirect redirect_to edit_study_path(#new_study),
URL: http://localhost:3000/studies/2/edit.
Is there anyway to customize an url after passing id ?
For example, http://localhost:3000/study
(still going to the edit path, and still with the :id in the params)
I guess what you want is to edit the current study?
In this case, it's possible, using ressource instead of ressources in the routes.
Let's have an example:
#in routes.rb
resources :studies
resource :study
Both of them will by default link to the StudiesController and call the same actions (eg. edit in your case) but in two different routes
get "/studies/:id/edit" => "studies#edit"
get "/study/edit" => "studies#edit"
in your edit action, you should then setup to handle correctly the parameters:
def edit
#study = params[:id].nil? ? current_study : Study.find(params[:id])
end
Note you need a current_study method somewhere, and store the current_study in cookies/sessions to make it works.
Example:
# In application_controller.rb
def current_study
#current_study ||= Study.find_by(id: session[:current_study_id]) #using find_by doesn't raise exception if doesn't exists
end
def current_study= x
#current_study = x
session[:current_study_id] = x.id
end
#... And back to study controller
def create
#...
#Eg. setup current_study and go to edit after creation
if study.save
self.current_study = study
redirect_to study_edit_path #easy peesy
end
end
Happy coding,
Yacine.

How to write a duplicate record method in Ruby on Rails?

In my Rails app I have an invoices_controller.rb with these actions:
def new
#invoice = current_user.invoices.build(:project_id => params[:project_id])
#invoice.build_item(current_user)
#invoice.set_number(current_user)
end
def create
#invoice = current_user.invoices.build(params[:invoice])
if #invoice.save
flash[:success] = "Invoice created."
redirect_to edit_invoice_path(#invoice)
else
render :new
end
end
Essentially, the new method instantiates a new invoice record plus one associated item record.
Now, what sort of method do I need if I want to duplicate an existing invoice?
I am a big fan of Rails's RESTful approach, so I wonder if I should add a new method like
def duplicate
end
or if I can use the existing new method and pass in the values of the invoice to be duplicated there?
What is the best approach and what might that method look like?
Naturally, you can extend RESTful routes and controllers.
To be rally RESTful, it is important to look exactly, what you want.
i.e. if you want a new invoice and use an existing one as a kind of template, then it is comparable to a new action, and the verb should be GET (get the input form). As is it based on an existing invoice, it should reference that object. After that you would create the new invoice in the usual way.
So in you routes:
resources :invoices do
member do
get 'duplicate'
end
end
giving you a route duplicate_invoice GET /invoices/:id/duplicate(.format) invoices#duplicate
So in your view you can say
<%= link_to 'duplicate this', duplicate_invoice_path(#invoice) %>
and in your controller
def duplicate
template = Invoice.find(params[:id])
#invoice= template.duplicate # define in Invoice.duplicate how to create a dup
render action: 'new'
end
If I understand correctly your question you can:
resources :invoices do
collection do
get 'duplicate'
end
end
and with this you can do:
def duplicate
# #invoice = [get the invoice]
#invoice.clone_invoice
render 'edit' # or 'new', depends on your needs
end
clone_invoice could be a custom method which should have a invoice.clone call in your custom method.
If you question if you can use additional methods except REST, you absolutely can. Google, for example, encourage developers to use something, what they call "extended RESTful" on GoogleIO, http://www.youtube.com/watch?v=nyu5ZxGUfgs
So use additional method duplicate, but don't forget about "Thin controllers, fat models" approach to incapsulate your duplicating logic inside model.

How Do I use redirect and pass an object to the edit action?

How Do I use redirect and pass an object to the edit action? The following code does not work:
def edit
#promotion = Promotion.find_by_id(#params['promo_id'])
end
def update
promotion = Promotion.find(params[:promotion_profile][:promotion_id])
promo_perfil = promotion.profile
if promo_perfil.update_attributes(params[:promotion_profile])
redirect_to admin_edit_path(promotion.id => #params[:promo_id])
else
end
The edit action is looking for params['promo_id'] (why #params['promo_id']?).
So pass :promo_id this way:
redirect_to admin_edit_path(:promo_id => promotion)
You shouldn't redirect in this case just: render :action => :edit
This is standard Rails behaviour, you will see this in all scaffolded controllers as a way of re rendering the edit form with the current object in the update action without having to rely on stuffing objects into cookies to pass around.

How to send form errors back to where they came from?

I have a page which shows a book and it's many reviews. On this page there is also a form to write a new review and post it to the create action of the reviews controller. When you post the form, the id of the corresponding book gets sent also so that the relationship can be established correctly.
At the reviews controller, we attempt to save the review. Here is what my controller looks like:
def create
#review = current_user.reviews.build(params[:review])
#book = Book.find_by_ean params[:book]
#review.book = #book
if #review.save
redirect_to book_path(#book)
else
# In here I want to go back to book_path(#book), sending #review with it so that I can have access to #review.errors
end
end
Of course, when the review fails to save (review content is mandatory for example) I would like to go back to the book show page and display the form, along with errors to the user. Now as far as I can work out, there are 2 possibilities here:
render "books/show", :review => #review --- This does send back the review with accompanying errors (I think, not 100% on this) but the URL stays as "/reviews" which causes a ton of it's own problems. For example partials which I keep in the "/books" directory can't be found.
redirect_to book_path(#book) --- This does get me back to the right URL but it doesn't send the #review with it so I can't show the error messages.
What's the best way to solve this problem?
I usually solve this by sending the psot data to a member action in the original controller (in this case books) instead of a nested controller. For example:
# routes.rb
resources :books do
member do
post 'create_review'
end
end
And then in your view
# books/show.html.erb
<%= form_for #new_review, :url => create_review_book_path(#book) do |f| %>
...
<% end %>
And finally in your books controller
# books_controller.rb
def create_review
#book = Book.find(params[:id])
#new_review = #book.reviews.build(params[:review])
if #new_review.save
#new_review = Review.new
end
render :action => :show
end
What happens is that the form has been manually directed to post it's data to our new member route in the books controller. If the Review is successfully saved, then we assign a new review object to the variable in preparation for the next review. If it is not succesfull, then the #new_review variable will contain the errors which can be accessed in the form.
Well, you can't really pass an object through the get params (with a redirect) unless you serialize it and you don't want that.
I see two solutions for this problem.
1) Save the review in the books controller
2) Make the form submit via ajax and update the form in the review controller using "render :update"
I don't know which one is the best, this depents on the project specs like can you use ajax?

How should I structure controller actions that share templates?

Let's say I have a User model, and an Invoice model with a belongs_to :user association.
Now I'm creating a new action for my InvoicesController, and the view that will be rendered. The view will have a select-element for selecting the user that this invoice will belong to.
So I need to fetch those users somewhere; my instinct is to leave this kind of thing out of the view. I end up with this:
def new
#users = User.all
end
The form submit action is create. When the creation fails for some reason, I re-render the new action's view.
def create
invoice = Invoice.new params[:invoice]
if invoice.save
flash[:notice] = 'Invoice created!'
redirect_to :action => 'show', :id => invoice.id
else
#users = User.all
render :action => 'new'
end
end
But as you can see, in order the re-render the new action, I have to fetch the users again.
This is just an example, but consider that I have some forms with several select-elements filled from the database, or similar constructs. That turns into an awful lot of repetition.
So how should I structure my controller in this situation?
Should I simply use User.all from my view?
Should I call new from within create to do the fetching for me?
Or something else?
For this I'd use a before_filter. For example you'd do something like:
before_filter :fetch_all_users, :only => [:new, :create]
protected
def fetch_all_users
#users = User.all
end
For 90% of my controllers I use the inherited resources plugin. It cuts down the amount of controller code you need to write for CRUD controllers, which also means you can cut down on the amount of tests you need to write.
For me:
What's the rails way to load other models collections for new, edit update and create actions?
It's not a good approach for my situation. Where after ".save", I send redirect_to to an another action, if I use before_filter and ".save" returns true, the fetch_all_users is called unnecessary

Resources