Rails 3 Nesting - ruby-on-rails

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

Related

Create views in resource in Rails

I am a beginner working with Rails: I have this routes.rb:
Rails.application.routes.draw do
resources :requirements
root "department#index"
get "department/about"
end
How can I create a view that has a path like requirements/major?
Thank you so much!
You can extend resources and add custom actions, like this:
resources :requirements do
collection do
get :major
end
end
You'll need an action in the RequirementsController that matches, e.g.
class RequirementsController < ApplicationController
def major
# set up whatever resource 'major' corresponds to
end
...
end
That's at least one way of doing it. You could also have a controller that directly supports the nested 'major' resource, which would be similar to above - just with a controller: 'name of controller' directive inline..
It'd probably pay to get your head around the "Rails Routing from the Outside In" guide: https://guides.rubyonrails.org/routing.html

Rails link_to polymorphic parent, which can have a nested route

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)

What are nested routes for in Rails?

I am new to learning Rails and have just encountered nested routes. The example I am looking at involves blog articles and comments. I am trying to undestand what the benefit of nested routes are in Rails.
As far as I can tell all the information contained in a nested route for a comment such as /articles/:article_id/comments/:id(.:format) is all contained in the comment object itself so it does not communicating additional information to the Action.
Why not just have unnested routes such as /comments/:id(.:format)?
There is obviously a very good reason for using nested routes but I havent been able to work it out. The only benefit I can see so far is it gives a better illustration of the relation between articles and comments when reading the URL but all this information is contained in the comment object anyway.
Could someone explain this?
In your model you would have setup this association
class Article< ActiveRecord::Base
has_many :comments
end
class Comment< ActiveRecord::Base
belongs_to :article
end
So each comment is associated with an article and you need some logic to find corresponding article for a comment
This is where nested route comes in and lets you find article for that comment in your controller action. If you look at that route again
/articles/:article_id/comments/:id(.:format)
This is the comment controllers show action and this route allows you to find both article and your comment inside show action
def show
#article = Article.find(params[:article_id])
#comment = Comment.find(params[:id])
# if you are not using nested routes then you can find out associated article by
#article = #comment.article # but you'll have to query your database to get it which you can simply find if you are using nested route
end
More than the show action(where you can use some other logic to find article associated with that comment) you need nested route for your new action where you have to find that article and then build a comment for that article by something like
def new
#article = Article.new
#comment = #article.comments.build
end
As #August pointed out you can separate out actions for which you want your route to be nested by using shallow nesting, you can do:
resources :articles do
resources :comments, shallow: true
end
Checkout nested routes for more information
Correct, having the article in the path is redundant when dealing with a preexisting comment (because you can get the article from the comment). To avoid this, you can use shallow routes:
#routes.rb
resources :articles, shallow: true do
resources :comments
end
# or use a `shallow` block
shallow do
resources :articles
resources :comments
end
end

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

Resources