How to use same RESTFUL name/route with same action? - ruby-on-rails

I have the models Dad, Mom and Kid. I'm already using the kid's index action for Dad and made another action in the Kids controller called mom_index for Mom's since a kid belongs to a mom and a dad.
Right now the Dad's route goes to:
dad_kids GET /dads/:dad_id/kids(.:format) kids#index
Because of:
resources :dads do
resources :kids
end
I need my Mom's route to go to:
mom_kids GET /moms/:mom_id/kids(.:format) kids#mom_index
But doing this:
resources :moms do
resources :kids
end
Uses the kid's index action which is already being used by the dad. How can I get it to do this using the Kids mom_index action instead?

You could just use an if statement to check whether the request is for dads or moms etc:
#app/controllers/kids_controller.rb
Class KidsController < ApplicationController
def index
if params[:mom_id].present?
# mom logic
elsif params[:dad_id].present?
#dad logic
end
end
end
An alternative would be to set different controllers (however this is not recommended as it's not DRY):
#config/routes.rb
resources :moms do
resources :moms_kids, as: "kids" #-> domain.com/moms/:mom_id/kids
end

You are better off either having the kids controller know how to deal with being used by either the dads or moms namespace or making two separate resources called dads_kids and moms_kids.
So for the first option you would leave what you have as far as routes but make the kids controller action smarter/overloaded.
For the second option you would do:
resources :dads do
resources :dads_kids
end
resources :moms do
resources :moms_kids
end
Then you would have 2 controllers dads_kids_controller.rb and moms_kids_controller.rb.

Related

Rails - Clean way to mix nested+non-nested or member routes?

New to programming in general and Rails specifically.
TLDR:
Is there a clean way of using a variety of member actions or controlling flow in controllers for nested resources? Is there a third option? I have read through Rails Routing from the Outside In and related stackoverflow posts, and haven't reached a satisfactory conclusion, so any help is appreciated.
Situation:
I would like for my users to be able to see all locations, and to see all locations in a specified group. (Locations also have groups, users have groups, etc).
Initially, I defined the routes and actions:
resources :groups do
member do
get :locations
end
end
resources :locations do
member do
get :groups
end
end
This worked fine, but I also need users to be able to create location in a specific group, etc.
I could define more member routes, e.g.
resources :groups do
member do
get :locations_index
get :location_new
end
end
resources :locations do
...
end
I have considered replacing the above with mixed nested and non-nested resources:
resources :groups do
resources :locations
end
resources :locations
etc.
The problem is that I am concerned the controllers will need a lot of flow control to ensure users see what I want them to see, can create locations in groups or not in groups, and so on.
Using before filters would help, but my controllers would still be much fatter than I'd like.
So, as above, is there a clean way of doing this?
You can use the module option to route the nested resources to a separate controller:
resources :locations, only: [:index]
resources :groups do
resources :locations,
only: [:index],
module: :groups
end
class LocationsController < ApplicationController
# Displays all locations
# GET /locations
def index
#locations = Location.all
end
end
module Groups
class LocationsController < ApplicationController
before_action :set_group
# Displays the locations of a group
# GET /groups/1/locations
def index
#locations = #group.locations
end
private
def set_group
#group = Group.find(params[:group_id])
end
end
end
It can be passed to most of the routing macros such as resource/resources, scope, namespace, member and collection.
This keeps the controllers lean since they are only handling a single representation of a resource.
The problem is that I am concerned the controllers will need a lot of flow control to ensure users see what I want them to see, can create locations in groups or not in groups, and so on.
Thats really the job of a separate authentication layer such as Pundit.
Try to avoid non REST routes.
You can have a look to this blog post http://jeromedalbert.com/how-dhh-organizes-his-rails-controllers/

How to structure controllers to match nested resources

My app is becoming very sensitive to context, as far as nested resources are concerned.
For example, let's say my app is a librarian app, and I might have routes like this:
resources :books
resources :libraries do
resources :books
end
And I want it to be such that if you visit /books, that you see books related to you, the logged in user. Be it books you've checked out or books you've favorited or what have you.
But when you visit /libraries/:id/books, you should see books related to that library.
Now, that's pretty easy to do in the controller:
def index
if params[:library_id]
#library = Library.find(params[:library_id])
#books = #library.books
else
#books = current_user.books
end
end
However, this pattern is repeated several times throughout my app (think if I wanted to have books nested under authors or publishers!) My controller can get very complex. It gets even harder if I want to have different views for each context.
So, I'd like to have two books controllers. One in the root namespace, and one namespaced under libraries.
That's fine. I've already constructed BooksController and Library::BooksController. However, I'm running into problems setting up sensible routes.
I first thought I could just specify the namespace:
resources :books
resources :libraries do
namespace :library do
resources :books
end
end
But that breaks the existing routes, looking like /libraries/:id/library/books
So, I tried passing path: false to the namespace, which fixes the route, but makes named routes and polymorphic routes very cumbersome.
I'd expect in order to visit /libraries/123/books, I could do:
link_to "See books", [#library, :books] # -or-
link_to "See books", library_books_path(#library)
However, since we've added the namespace, routes are pretty bulky now:
link_to "See books", [#library, :library, :books] # -or-
link_to "See books", library_library_books_path(#library)
So, is there a more conventional way to structure controllers that make sense with nested resources? Is there a better way to construct my routes?
UPDATE
I've got expected results by adding as: false to the namespace declaration like so:
resources :books
resources :libraries do
namespace :library, path: false, as: false do
resources :books
end
end
And H-man noted that you can specify a controller on each resource. However, that doesn't feel right as management of a large number of routes could get out of control.
So both solutions work, but is there a more conventional way to approach this?
UPDATE #2
I've landed on using #defaults from ActionDispatch::Routing::Mapper::Scoping which allows you to send arguments straight to #scope without a bunch of other interfering logic. I like this better since rather than setting negatives, I'm setting positive configuration:
resources :books
resources :libraries do
defaults module: :libraries
resources :books
end
end
However, the question still stands if I'm following good convention... This still doesn't feel 100% right.
Try using scoped modules:
resources :books
scope module: :libraries do
resources :libraries do
resources :books
end
end
Then you should have these sorts of helpers available to get to the index actions (for example):
books_path == [:books]
library_books_path(#library) == [#library, :books]
I get torn on whether or not the libraries resource should be under the scoped module, but most of the time it makes sense to do so. Pretend that you want to take that chunk out and put it in a gem later (even if that's not your plan). That helps to draw the line on what goes in each scoped module.
If you want to specify a different controller for the nested route, you can always do this:
resources :books
resources :libraries do
resources :books, controller: 'libraries/books'
end
That way, you can have two controllers:
app/controllers/books_controller.rb
app/controllers/libraries/books_controller.rb
What I usually do in this case, is let each controller controller handle the scoping, and put the shared functionality in a concern. i.e:
app/controllers/books_controller.rb
class BooksController < ApplicationController
include BooksControllerConcern
private
def book_scope
current_user.books
end
end
app/controllers/libraries/books_controller.rb
class Libraries::BooksController < ApplicationController
include BooksControllerConcern
private
def book_scope
Book.find_by(library_id: params[:library_id])
end
end
concern:
module BooksControllerConcern
extend ActiveSupport::Concern
def index
#books = book_scope.page(params[:page])
end
# .. other shared actions
end
Your routes will look like this:
[:books] or books_path => /books
[#library, :books] or library_books_path(#library) => /libraries/:id/books

How can i create the following route

Rails beginner, so please don't bite. I've taken over the maintenance/development of a rails an app, but still learning the ropes
I'd like to generate the following route :
/events/1/Project/2/Pledge
Where 1 is the eventId and 2 is the Project Id.
I have a project controller and and events controller. The pledge action is on the Project controller
EDIT: In answer to #wacko's comment below.
a)Ignore the casing and pluralization of the url i asked for (I realise that invalidates the original question somewhat...)
An event has multiple projects, The pledge action will take the user to a page where they can enter multiple pledges for a particular project.
Perhaps instead the Pledge action should be on the Events Controller instead?
and the URL something like 'events/1/pledge/2' (Where 2 is the projectId)
What you are looking for is called a nested resource, that is to say that there is a parent child relationship between two resources.
resource :events do
resource :projects do
get :pledge, :on => :member
end
end
For this to work, your models would look something like this
class Event < ActiveRecord::Base
has_many :projects
end
And
class Project < ActiveRecord::Base
belongs_to :event
end
The following should work
get '/events/:event_id/projects/:id/pledge' => 'projects#pledge'
In your controller action you can get the event_id and project_id from the params hash as params[:event_id] and params[:id] respectively
resources :events do
resource :projects do
resources :pledge
end
end
this will give you the ability to set the scope in your controllers and have access to all 7 REST verbs
resources :events do
resources :projects do
member do
get :pledge
end
end
end
You can change get to the http method you want.
You can use collections if you need a route like /events/1/projects/pledge
collection do
get :pledge
end
run rake routes from the project root folder to see a list of routes generated
jsut use this way
resources :events do
resource :projects do
get '/pledge'
end
end

Rails 3 Nesting

I have a 2 level nesting objects that i need help with
My routes look like this to normalise the url abit. instead of having a url that looks like this /projects/1/tasks/3/comments/3.
resources :projects do
resources :tasks
end
resources :tasks do
resources :comments
end
Model has the 'has_many', belongs_to methods.
I can create comments under each task and display them under the tasks, but on the 'show' template of the comments i would like to display a link back to the tasks, which i get an error because the tasks controller is asking for a project_id?
How would this normally done when dealing with 2 level nesting?
I would do
resources :projects, :shallow => true do
resources :tasks do
resources :comments
end
end
Which is basically what you're doing except you can't generate a projects_task member path(ie projects/1/tasks/1) anymore they'd all just be task member paths(ie '/tasks/1').
Member paths include show, update, delete, edit
But the project_tasks collection paths(ie projects/1/tasks) would still be available.
Collection paths include index, create, new
comment paths wouldn't change. All comment paths would still include the task/:task_id prefix.
Checkout the resources documentation for more info on that (also more info on member and collection also on that page.)
But to actually solve your problem
You need to look up the project_id when you link back to the project_tasks index. So you would need to do
<%= link_to "Project Tasks Index", project_tasks_path(#task.project) %>
That way the Task#index knows where the parent project is. This is the solution for both implementations.
Check out the UrlFor documentation for more info on that.
If you want access to a #project variable in a Comment view
Then you just need to look up the project in the controller instead of at a view level. So basically
class CommentsController < ApplicationController
def show
#task = Task.find(params[:task_id])
#comment = #task.comments.find([:id])
#project = #task.project
end
end

Rails 3 Nested Routing

I have very interesting scenario:
I've specified two controllers, one for global events and another another once for company specific events. In routes, it is specified like this:
resources :companies do
resources :events
end
resources: events
Running rake routes I can see the routes being generated:
events GET /events(.:format) events#index
company_events GET /companies/:company_id/events(.:format) events#index
Both paths seem to route to the same controller (the global one)...
I have the second controller under controller/companies that goes something like this:
class Companies::EventsController < ApplicationController
# stuff
end
It never routes in that controller above, no matter whether I use company_evens_path(#company). always goes to the other one.
It used to work in rails 2.3 for me, I'm currently using 3.2
Ok as stated above, I would recommend doing something like this:
def index
if params[:company_id]
#events = Company.find(params[:company_id]).events
else
#events = Events.all
end
end
although if you need to you can specify a controller:
resources :companies do
resources :events, :controller => "companies/events"
end
resources: events
and just create a companies folder inside your controllers folder to put your Companies::EventsController inside

Resources