Rails - Conventions for Complex & Nested Routes (non-resourceful) - ruby-on-rails

I have some more complex routes setup in my app, that I'm wondering could be turned into resourceful routes.
What are the ideal Rails conventions for turning these into resourceful routes?
Route 1: /grandparent-place/parent-place/place/
These routes are at the bottom of my routes.rb file, as they pull from the root path and are scoped by parents and children.
Routes.rb
get ':grandparent_id/:parent_id/:id', to: 'places#show', as: :grandparent_place
get ':parent_id/:id', to: 'places#show', as: :parent_place
get ':id', to: 'places#show', as: :place
Places_Controller.rb
def set_place
if params[:parent_id].nil? && params[:grandparent_id].nil?
#place = Place.find(params[:id])
elsif params[:grandparent_id].nil?
#parent = Place.find(params[:parent_id])
#place = #parent.children.find(params[:id])
else
#grandparent = Place.find(params[:grandparent_id])
#parent = #grandparent.children.find(params[:parent_id])
#place = #parent.children.find(params[:id])
end
end
Application_Helper.rb
def place_path(place)
'/' + place.ancestors.map{|x| x.id.to_s}.join('/') + '/' + place.id.to_s
end
Route 2: /thread#post-123
These routes are meant to allow for only specific actions, using the parent module to specify the controller directory - and using a # anchor to scroll to the specified post.
Routes.rb
resources :threads, only: [:show] do
resources :posts, module: :threads, only: [:show, :create, :update, :destroy]
end
Application_Helper.rb
def thread_post_path(thread, post)
thread_path(thread) + '#post-' + post.id.to_s
end
Is it convention to override the route paths in the application helper, or is there a better way to generate the correct URLs without overriding the helpers?

Path variables are used to specify resources and usually one variable specify one resource. For example:
get '/publishers/:publisher_id/articels/:article_id/comments/:id'
In your setup you have places as a resource.
So, in this endpoint get '/places/:id' :id specifies which place should be retrieved.
Regarding your first route it would be most appropriate to leave just one get endpoint:
resource :places, only: [:show] # => get '/places/:id'
and pass the id of the parent or grandparent as :id whenever you need to retrieve the parent or grandparent place. That way you won't need any conditions in set_place method and so have:
def set_place
#place = Place.find(params[:id])
end
In case you need to access parents or grandparents of a place object you can build:
get '/places/:place_id/parents/:parent_id/grandparents/:id'
or just leave get '/places/:place_id/parents/:id' and whenever you need to access a grandparent just make the call starting from your parent place instead of the child. Route setups can vary depending on your needs. Rails provides various examples on that matter: Rails Routing from the Outside In
Regarding the helpers, there isn't a general rule for overriding or not your path methods and again it mostly depends on the application's needs. I think it is a good practice to keep them intact as much as possible. In your case instead of overriding the path method you can place:
thread_posts_path(thread) + '#post-' + post.id # => /threads/7/posts#post-15
directly in your view, for example:
link_to 'MyThredPost', thread_posts_path(thread) + '#post-' + post.id

Related

New action not being reached in my Ruby app

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.

Use same controller with associations in top-level and nested paths in Rails

I would like my REST API to have several routes, such as:
GET /posts/
GET /posts/1
POST /posts
GET /users/
GET /users/1
GET /users/1/posts
POST /users/1/posts
Is it possible to reuse the same controller for those nested routes under the users collection?
It looks like you want nested routes. Try this is your config/routes.rb
resources :posts
resources :users do
resources :posts
end
This has more info. You could also use match or post and get verb methods individually. There are also many options for nested routes.
https://guides.rubyonrails.org/routing.html.
ALTERNATIVELY
in config/routes.rb:
get 'users/:id/posts', to: 'users#posts'
and in controllers/users_controller.rb
before_action :set_user, only: [:users_posts, :show, :edit, :update, :destroy]
...
def posts
#posts = #user.posts
end
With the second option you can KISS by keeping POST/PATCH/UPDATE/DESTROY at their native home like /posts and /posts/42. Just treat :user_id as a form variable in that case, with whatever extra validation you might need, perhaps referencing a session var.
LASTLY
You can actually put this in your config/routes.rb. But now you're probably writing new forms because :user_id is a route parameter. I'd file that under extra complexity. Maybe it fits your situation though.
post 'users/:id/posts', to: 'users#posts_create'

What is the path for this route?

get 'users/:id/edit/settings' => 'users#account'
What is the dry way to reference this path in link_to?
As a side note, I use 'users/:id/edit' to edit name/location/age etc and I am using the route above to edit password and email, because I wish to force the user to authenticate their :current_password before editing these more sensitive attributes. I mention this just to make sure my routing logic is correct.
Just run rake routes and you will see all the routes that you have in you app. It should be to the far right
You can use the as: option to setup a named route.
However I would set it up with conventional rails routes:
Rails.application.routes.draw do
resources :users do
resource :settings, only: [:edit, :update], module: :users
end
end
This would create an idiomatically correct RESTful route.
Using the singular resource creates routes without an id parameter. Also you should only use the name :id for the rightmost dynamic segment in a route to avoid violating the principle of least surprise.
rake routes will show you the following routes:
Prefix Verb URI Pattern Controller#Action
edit_user_settings GET /users/:user_id/settings/edit(.:format) users/settings#edit
user_settings PATCH /users/:user_id/settings(.:format) users/settings#update
PUT /users/:user_id/settings(.:format) users/settings#update
...
As a side note, I use 'users/:id/edit' to edit name/location/age etc
and I am using the route above to edit password and email, because I
wish to force the user to authenticate their :current_password before
editing these more sensitive attributes. I mention this just to make
sure my routing logic is correct.
Your route will in no way enforce this authorization concern.
Instead you should do a check in your controller:
# app/models/users/settings_controller.rb
class Users::SettingsController
before_action :set_user
before_action :check_password, except: [:edit]
def edit
# ...
end
def update
# ...
end
private
def set_user
#user = User.find(params[:user_id])
end
def check_password
# this is an example using ActiveModel::SecurePassword
unless #user.authorize(params[:current_password])
#user.errors.add(:current_password, 'must be correct.')
end
end
end
change it to:
get 'users/:id/edit/settings' => 'users#account', as: :edit_user_settings
and then you can just reference it as:
link_to edit_user_settings_path(#user)
rake routes will probably give you a path something like users_path which you can link to using something like
<%= link_to 'Users', users_path(#id) %>

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)

Getting 'Unknown action in controller'

I'm getting this error :
"The action 'create' could not be found for ObjectController"
I know it should be obvious but I'm missing something, that's my controller :
class ObjectController < ApplicationController
def index
end
def create
end
end
And that is my routes :
Rails.application.routes.draw do
get 'object/index'
get 'object/create'
match ':controller(/:action(/:id))', :via => :get
resources :objets
# The priority is based upon order of creation: first created -> highest priority.
# See how all your routes lay out with "rake routes".
# You can have the root of your site routed with "root"
root 'object#index'
You probably want to scrap those routes and try something simpler like
resources :objects, only: [:get, :create, :show]
Then use
$ rake routes
To make sure your routes are as the should be. You will want a POST route to /objects to create a new object etc..
Ok that one was dumb, actually I had two directories and I wasn't modifying the right one, sorry about that...
Your routes could be greatly improved:
#config/routes.rb
Rails.application.routes.draw do
root 'objects#index'
resources :objects
--
Next, the "standard" way to achieve what you're looking for is to use the new action; IE not the "create" action. If you wanted to use the create path name (instead of new), you'll be able to define it in the path_names argument:
#config/routes
resources :objects, path_names: { new: "create", create: "create" } #-> url.com/objects/create
To understand why you should be using new instead of create, you should look up resourceful routing, and how it pertains to object orientated programming.
Finally, your controller should be named in the plural:
#app/controllers/objects_controller.rb
class ObjectsController < ApplicationController
...
end
Whilst you can call it whatever you like, Rails defaults to plural controller names, singular model names.

Resources