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
Related
I have a rails app where many of the models are editable using best_in_place, so I have a lot of controllers that look partially like this:
before_action :find_objects, except: [:new, :create]
def update
#object.update_attributes(object_params)
respond_to do |format|
format.json { respond_with_bip #object}
end
end
private
def object_params
params.require(:object).permit(:foo, :bar)
end
def find_objects
#object = Object.find(params[:id])
end
How do I move this particular repeated piece into a controller concern, given that the object being updated is going to come in with a particular name in the params hash, and object_params and find_objects should call their proper versions based on the model name? Is there some elegant meta-magic that'll sort this all out?
I think this is a case where your code could be "too DRY". You can certainly accomplish this using meta-magic, but it could make your code confusing in the long run.
If you want to do the meta-magic, one trick is to use params[:controller] to get the name of the model. For example, if you have a PostsController, then:
params[:controller] # => "posts"
params[:controller].classify # => "Post"
Taking this a step further, you could write a generic find_object like this:
def find_object
model_class = params[:controller].classify.constantize
model_instance = model_class.find(params[:id])
instance_variable_set("##{model_class.name.underscore}", model_instance)
end
But as I said at the beginning, I'm not sure I would recommend this amount of abstraction just for the sake of DRY-ing your controller code.
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.
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.
Perhaps this can even become a Community Wiki, but I would love a detailed description of how the controller works - or rather, how I can get it to do what I want it to do.
I understand the general structure of MVC and how the model stores the db structure, and the controller interacts with the db and passes info to the view.
However, I am puzzled (on a fundamental level) about how to accomplish simple tasks using my controller. I know that if I want to create a new record for a model/object, I just do object = Object.new(:name => "Object Name") in the Rails console.
But how on earth would I do that in the CRUD elements of the controller and why?
Please use a simple example - e.g. showing a user the balance of their bank account (I know there are many complexities surrounding this, but ignore them for the sake of this explanation). What would the model look like (just include: Name, Address, Transaction Type (Deposits/Withdrawals), Balance).
What would a view look like? What would the controller look like? Any choices you make (like using a form) please explain them. Why would you use a form, as opposed to a drop down menu and (in layman terms) how does the form or drop down menu interact with the controller? How do I get the info captured there to the db and why am I doing it that way?
I know this sounds like a lot to ask, but I have done RailsTutorial.org, watched many Railscasts, read the Rails guides, and read many other tutorials and still have some basic gaps in my understanding of the way Rails works and why.
Thanks in advance.
I don't know how much more help I can be, but I understand your pain having just come to rails myself. The article recommended by ghoppe, "Skinny Controller, Fat Model" explains the function of Ms Vs & Cs nicely. Seeing as that does not fully answer your question I will try to explain the mechanics of each structure.
Model
class Account < ActiveRecord::Base
belongs_to :user
validates_presence_of :address
def name # Account does not have a name field, but User does so I will make a name method for Account and feed it name of the user it belongs to.
user = self.user # Account gets the user method with the <belongs_to :user> association
# note: Rails expects Accounts to have a user_id field so it can perform the "magic" to associate Accounts with Users
if user.name
return user.name
else
return nil
end
end
end
The model describes your object. Like an object in any OOP language you want to put all of your object logic here. This includes the rails helpers for association(has_one, belongs_to, ...) and validation, as well as any other method or library you want the object to be able use throughout your Models Views and Controllers.
Controller
class AccountsController < ApplicationController
before_filter :name, :only => :edit, :destroy # #account.name will be executed before the edit or destroy method(action) can be invoked on #account. If the user who has the account has a name the action will execute.
def index # This is a RESTful action and is mapped by Rails by default to an HTTP GET request. Rails expects an index.html.erb or index.haml.erb or index.something in the Accounts view to map this action to.
#accounts = Account.all # #accounts is an instance variable and will be accessible in the view this action is mapped to.
end
def show
#account = Account.find(params[:id]) # params[:id] is passed to the controller from the view. The params hash is the primary tool form moving data from a form or URL into a controller. Anytime you click on the link_to the show or edit action of an object Rails will put that objects id in the params hash and call the appropriate action in that objects controller. If you click the show link on an account it will call this action. Now the instance variable in the view show.html.erb will hold a single account instead of an array
end
def new
#account = Account.new # This initializes a new account with all the fields set to blank unless you specified a default in your migration. This account has not been save to the db yet. It is ready for a user to fill in.
respond_to do |format| # Rails can automatically respond differently to different client request. If a client i.e browser wants HTML rails responds with HTML. If a client e.g. an API want XML Rails responds with XML.
format.html # new.html.erb #
format.xml { render :xml => #account }
end
end
def edit
#account = Account.find(params[:id]) # Same as show, but mapped to a different view
end
def create # Finally we have a POST. All the prior actions were GETs, but now we are saving some data to the db.
#account = Account.new(params[:account]) # The :account key is special. It is a hash of hashes. It is populated by the form fields in new.html.erb. To access a specific field such as address we say <params[:account][:address]> and whatever the user entered in the address field in the View is at out fingers in the Controller.
respond_to do |format|
if #account.save # If the validations pass and the account gets saved redirect to the show page of the new record, otherwise refresh/render the new page (hopefully showing what error caused the record to fail to save).
format.html { redirect_to(#account, :notice => 'Account was successfully created.') }
format.xml { render :xml => #account, :status => :created, :location => #account }
else
format.html { render :action => "new" }
format.xml { render :xml => #account.errors, :status => :unprocessable_entity }
end
end
end
def update # This is another of the seven RESTful Rails actions and results in a PUT request because you are updating an existing record
#account = Account.find(params[:id])
respond_to do |format|
if #account.update_attributes(params[:account])
format.js # Rails can also respond with JavaScript. Look up UJS. Rails 3 has made large improvements here.
format.html { redirect_to(#account, :notice => 'Account was successfully updated.') }
format.xml { head :ok }
else
format.js
format.html { render :action => "edit" }
format.xml { render :xml => #account.errors, :status => :unprocessable_entity }
end
end
end
def destroy # This results in a DELETE
#account = Account.find(params[:id])
#account.destroy # destroy is a more thourough delete and will check the options of this records associations and destroy the associated objects as well if they are dependant on this object. The option <:dependant => :destroy> is not set for this object's only association: User. The user this account belongs to will therefore survive the destruction of this account.
respond_to do |format|
format.html { redirect_to(accounts_url) }
format.xml { head :ok }
end
end
end
View
Hopefully you can draw your own logic from here. The view is designed to render information passed as instance vars from a controller to a client: browser, api, smart phone. As well as to pass information from a client to the controller via the params hash. No complicated logic should get performed in a view even though a view with erb has the capability to execute any ruby code.
If an example view would also be helpful I am happy to oblige.
The best description of what the controller is:
http://edgeguides.rubyonrails.org/action_controller_overview.html
http://edgeguides.rubyonrails.org/routing.html
The controller doesn't communicate with the Database. The controller talks to the model, which then communicate with the database.
When I was starting I found very useful to use scaffolding and just looking at what was created.
Do this:
rails generate scaffold Post name:string title:string content:text
Examine all files under the app/ folder. Examine the file config/routes
Then comment here your specific questions.
At first, I thought this question was far too broad, along the lines of "how do I program?" But after reading your comments, I see what you're getting at. You don't quite grasp how MVC works in Rails and are wondering where your code goes.
What you should strive for is a Skinny Controller and a Fat Model. Keep logic out of views. So in your example, you calculate the account balance in the Model, and pass that information along (using the controller) to the view.
For a concise explanation for beginners with sample code, I recommend this article over here.
I have a fairly simple model; Users have_many products. I would like to be able to view a list of all products as well as a list of the products associated with a given user. My routes are set up like this:
/products
/products/:id
/users
/users/:id
/users/:id/products
The catch here is that I'd like to display the product list differently in the product#index view and the user/products#index view.
Is there a 'correct' way to do this? My current solution is to define products as a nested resource inside users, and then to check for params[:user_id] - if its found I render a template called 'index_from_user', otherwise I just render the typical 'index' template.
This is a situation I'm running into a lot - if there's a preferred way to do it I'd love to know...
You can declare two "products" routes - one under users, and one independent of users eg:
map.resources :products
map.resources :users, :has_many => :products
They will both look for "ProductsController#index" but the second will have the "user_id" pre-populated from the route (note: "user_id" not just "id")
So you can test for that in the index method, and display different items depending on whether it is present.
You will need to add a before_filter to the ProductController to actually instantiate the user model before you can use it eg:
before_filter :get_user # put any exceptions here
def index
#products = #user.present? ? #user.products : Product.all
end
# all the other actions here...
# somewhere near the bottom...
private
def get_user
#user = User.find(params[:user_id])
end
If you really want to display completely different views, you can just do it explicitly in the index action eg:
def index
#products = #user.present? ? #user.products : Product.all
if #user.present?
return render(:action => :user_view) # or whatever...
end
# will render the default template...
end