I have the Comment model, which is polymorphic associated to commentable models like Project, User, Update etc. And I have a page where a user can see every User's comment. I want a link near each comment with an address of an object this comment is associated with.
I could write something like that:
link_to 'show on page', Object.const_get(c.commentable_type).find(c.commentable_id)
But this will work only for not nested routes (like User). Here's how my routes look like:
resources :users do
resources :projects, only: [:show, :edit, :update, :destroy]
end
So when I need a link to a Project page, I will get an error, because I need a link like user_project_path.
How can I make Rails to generate a proper link? Somehow I have to find out if this object's route is nested or not and find a parent route for nested ones
You could use a bit of polymophic routing magic.
module CommentsHelper
def path_to_commentable(commentable)
resources = [commentable]
resources.unshift(commentable.parent) if commentable.respond_to?(:parent)
polymorphic_path(resources)
end
def link_to_commentable(commentable)
link_to(
"Show # {commentable.class.model_name.human}",
path_to_commentable(commentable)
)
end
end
class Project < ActiveRecord::Base
# ...
def parent
user
end
end
link_to_commentable(c.commentable)
But it feels dirty. Your model should not be aware of routing concerns.
But a better way to solve this may be to de-nest the routes.
Unless a resource is purely nested and does not make sense outside its parent context it is often better to employ a minimum of nesting and consider that resources may have different representations.
/users/:id/projects may show the projects belonging to a user. While /projects would display all the projects in the app.
Since each project has a unique identifier on its own we can route the individual routes without nesting:
GET /projects/:id - projects#show
PATCH /projects/:id - projects#update
DELETE /projects/:id - projects#destroy
This lets us use polymorphic routing without any knowledge of the "parent" resource and ofter leads to better API design.
Consider this example:
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
resources :projects
resources :users do
# will route to User::ProjectsController#index
resources :projects, module: 'user', only: [:index]
end
end
class ProjectsController < ApplicationController
def index
#projects = Project.all
end
# show, edit, etc
end
class User::ProjectsController < ApplicationController
def index
#user = User.joins(:projects).find(params[:user_id])
#projects = #user.comments
end
end
This would let us link to any project from a comment by:
link_to 'show on page', c.commentable
And any users projects by:
link_to "#{#user.name}'s projects", polymorphic_path(#user, :projects)
Related
I'm just learning Ruby on Rails and building a fairly simple app that stores info to a rake generated db. I'm trying to add a new action to the controller but I can't trigger it. There's a before_action getting triggered which shouldn't be.
class GamesController < ApplicationController
before_action :set_entry, only: %i[ show edit update destroy ]
before_action :authenticate_user!
def index
end
def analyse
#The function here doesnt matter, its not reaching it because its hitting the set_entry instead
puts 'howdy'
end
private
def set_entry
#entry = Entry.find(params[:id])
end
end
The error I'm hitting is Couldn't find Game with 'id'=analyse", highlighting the set_entry action, which to my understanding should only be running with the show, edit, update, and destroy actions (which are also in the full controller and running fine). There are other actions in the controller (such as create and new) which don't seem to trigger that set_entry and are running just fine as expected. For example, linking to the new path takes me to /entry/new, while linking to an edit path takes me to /entry/:id/edit, which is all fine. But it keeps linking my new analyse action trying for entry/:id/analyse when I want it to go to entry/analyse.
My button to trigger it is simply:
<%= link_to "Analyse", analyse_path %>
Which is in a navbar in my application.html.erb
And here's my routes.rb:
Rails.application.routes.draw do
resources :entry
devise_for :users
resources :users
root to: "entry#index"
get 'entry/index'
get 'entry/analyse', as: 'analyse'
end
The path /entry/analyse is matched two routes:
entry#show action in resources :entry
entry#analyse action in get 'entry/analyse', as: 'analyse'
Because the route matching is interpreted through config/routes.rb in order, and resources :entry is the first route the path is matched. At the result, entry#show action will handle the requests from /entry/analyse.
The solution is simple, just switch the order of resources :entry and get 'entry/analyse', as: 'analyse' in config/routes.rb. For example:
Rails.application.routes.draw do
get 'entry/analyse', as: 'analyse' # <- to here
resources :entry
devise_for :users
resources :users
root to: "entry#index"
get 'entry/index'
# from here ...
end
Move all of your resources to the end of your route configuration. I am not sure why its getting tripped up but it seems something within your routes is matching analyse to your entry resource and routes within rails are matched in order. All custom routes really should come first to prevent something like a generic route catching your expected action.
Best practices also state your root to: should be at the top of the configuration since it is your most popular route generally in an application.
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/
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
I know that in a nested resource, you have to pass the id of the containing resource for it to work.
However, I want to allow users to create a review for a product and specify the product within the form in the new view rather than actually pass it in in the route. So rather than being on a product and then review it, there would be a master review form where you can select any product within the form.
I am getting an error trying to do this and can't figure it out - what is the solution?
#error
No route matches {:action=>"new", :controller=>"reviews"}
#view
new_product_review_path
#controller
def new
##product = Product.new
#review = Review.new
end
#routes.rb
resources :products do
resources :reviews
end
Keep reviews nested within products, but move the creation for reviews outside:
# routes.rb
resources :products do
resources :reviews, except: [:new, :create]
end
resources :reviews, only: [:new, :create]
Then use new_review_path to create a new review.
If you wanted to have the option to create a review either attached to a particular product or not, drop the :except option for your nested review, and modify your controller action and form based on product_id.present?.
EDIT: (In response to the Zephyr's follow up question)
In the example above, I've specified that the :new and :create should only be accessible outside products, and not inside it by using the :except and :only options for reviews -- feel free to drop them if you'd like to be able to do anything with reviews regardless of whether it's nested or not. The only thing to be careful of then is that the same controller actions will handle the requests regardless of whether it's nested or not -- reviews#index will handle both /reviews and /products/1/reviews, for example -- so you'll need to make sure that your controller handles that properly. For example, using a before filter:
# users_controller.rb
before_filter :filter_by_product
def filter_by_product
#parent_product = Product.find(params[:product_id]) if params[:product_id]
#reviews = #parent_product ? #parent_product.reviews : Review.all
end
def new
#review = #reviews.build
end
...
You may also need to make changes to your view based whether it's nested or not. For example, in your form to create a review, you could use a drop down of products if it's not nested or replace that with a hidden field with the product_id if it is.
You can use namespace thing to wrap your route with something else
Move resources :reviews outside the block
resources :products
resources :reviews
and use
new_review_path
This way, your reviews are not tied down to any product.
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