How to share a controller action for nested routes in Rails? - ruby-on-rails

In Rails, when I need:
/comments
and
/posts/1/comments
How do I best organize CommentsController? E.g. let the routes share the index actions, or work with 2 controllers?

You can work with only one controller.
I would go with a before_filter to check if the post_id param is present:
class CommentsController < ApplicationController
before_filter :find_post, only: [:index]
def index
if #post.present?
## Some stuff
else
## Other stuff
end
end
private
def find_post
#post = Post.find(params[:post_id]) unless params[:post_id].nil?
end
end
And have in your routes (with the constraints of your choice) :
resources :posts do
resources :comments
end
resources :comments

I believe you want /comments only for show and index actions, right? Otherwise the post params will be lost when creating or updating a comment.
In your routes.rb you can have something like:
resources : posts do
resources :comments
end
resources :comments, :only => [:index, :show]
In your form:
form_for([#post, #comment]) do |f|
And in your controller, make sure you find the post before dealing with the comments (for new, edit, create and update, such as:
#post = Post.find(params[:post_id])
#comment = #post...

You can do almost anything you want with rails routes.
routes.rb
match 'posts/:id/comments', :controller => 'posts', :action => 'comments'}
resources :posts do
member do
get "comments"
end
end

Related

Rails 5 routing resources using custom actions

About routing, If I do something like this:
resources :students
resources :teachers
I will get something like:
students GET /students(.:format) students#index
...
teachers GET /teachers(.:format) teachers#index
...
Changing to:
resources :students, controller: :users
resources :teachers, controller: :users
will give me:
students GET /students(.:format) users#index
teachers GET /teachers(.:format) users#index
Note that now, both resources are using the same controller Users and the same action index. But what I need, instead of using the same index action, is the students resource to use actions prefixed by students like students_index and teachers resources prefixed by teachers like teacher_index.
In other words, I want bin/rails routes to give me the following output:
students GET /students(.:format) users#students_index
teachers GET /teachers(.:format) users#teachers_index
I know that I can do the same with:
get 'students', to: 'users#students_index'
But there is a way to do the same with resources?
I don't think there's a way to do that with resources helper. What you could do (if it's only the index action you wanna override) is add an except, like this:
resources :students, controller: :users, except: [:index]
resources :teachers, controller: :users, except: [:index]
then, as you already suggested, do the individuals index actions like that:
get 'students', to: 'users#students_index', as: :student
get 'teachers', to: 'users#teachers_index', as: :teacher
Or you could reconsider the structure of your controllers... Good luck!
There is a far better way to do this as you might have surmised - inheritance.
# app/controllers/users_controller.rb
class UsersController < ApplicationController
delegate :singular, :plural, :param_key, to: :model_name
before_action :set_resource, only: [:show, :edit, :update, :destroy]
before_action :set_resources, only: [:index]
def initialize
#model_name = resource_class.model_name
super
end
def show
end
def index
end
def new
#resource = resource_class.new
set_resource
end
def create
#resource = resource_class.new(permitted_attributes)
if #resource.save
redirect_to #resource
else
set_resource
render :new
end
end
def edit
end
def update
if #resource.update(permitted_attributes)
redirect_to #resource
else
set_resource
render :edit
end
end
def destroy
#resource.destroy
redirect_to action: "index"
end
# ...
private
# Deduces the class of the model based on the controller name
# TeachersController would try to resolve Teacher for example.
def resource_class
#resource_class ||= controller_name.classify.constantize
end
# will set #resource as well as #teacher or #student
def set_resource
#resource ||= resource_class.find(params[:id])
instance_variable_set("##{singular}", #resource)
end
# will set #resources as well as #teachers or #students
def set_resources
#resources ||= resource_class.all
instance_variable_set("##{plural}", #resources)
end
def permitted_attributes
params.require(param_key).permit(:a, :b, :c)
end
end
# app/controllers/teachers_controller.rb
class TeachersController < UsersController
end
# app/controllers/students_controller.rb
class StudentsController < UsersController
end
# routes.rb
resources :students
resources :teachers
This lets you follow the regular Rails convention over configuration approach when it comes to naming actions and views.
The UsersController base class uses quite a bit of magic through ActiveModel::Naming both to figure out the model class and stuff like what to name the instance variables and the params keys.

What is the RESTful and Rails way to add these actions to my controller

I am adding tagging functionality to my app, via acts_as_taggable_on.
That gem doesn't add controllers, but I would like to. I am adding the tagging functionality to my Node model.
On my NodeController, I know I could simply add the explicit actions like this:
def add_tagged_user
end
def remove_tagged_user
end
def tagged_users
end
But that doesn't feel very restful or Railsy.
The corresponding route would look like this:
resources :nodes do
match :add_tagged_user, via: [:post], on: :member
match :remove_tagged_user, via: [:delete], on: :member
match :tagged_users, via: [:get], on: :member
end
Is there a RESTful or a more Railsy way to do this?
You could go with a single TagsController, with routes that match the RESTful resource(s).
Something like
Routes
# routes.rb
resources :nodes do
resources :tags, only: [:show, :create, :update]
end
resources :other_resources do
resources :tags, only: [:show, :create, :update]
end
Controller
class TagsController < ApplicationController
before_action :load_taggable
def create
#taggable.tags.create(tag_params)
end
private
def load_taggable
# switches on params
#taggable = if params[:node_id]
Node.find(params[:node_id]
elsif # other things that are taggable
# OtherThing.find(...)
end
end
end

Display all comments and order them

I want to display all comments (total number) on 'all' page. So, not all comments for a specific Post, but all comments in the entire app. I've tried with Comment.all, but it says it can't find post without an ID...
.../comments/all
routes
resources :posts do
resources :comments do
member do
put "like", to: "comments#upvote"
put "dislike", to: "comments#downvote"
end
end
end
comments_controller
def all
?
end
def index
#post = Post.find(params[:post_id])
#comments = #auto.comments.order("cached_votes_score DESC")
end
def show
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
end
...
You need a not nested route to comments:
routes
resources :posts do
resources :comments do
member do
put "like", to: "comments#upvote"
put "dislike", to: "comments#downvote"
end
end
end
get "comments#all"
comments_controller
def all
#comments=Comment.all
end
The problem is in your PostsController because what you want is not actually directly relevant to a post.
Try adding a collection route for comment
resources :comments do
member do
..
end
collection do
get :all # actually that is index
end
end
or simpler
#config/routes.rb
resources :comments, only: :index
and then a
#app/controllers/comments_controller.rb
def index
Comment.all
end
on your CommentsController will do.

AbstractController::ActionNotFound for destroy action

I am facing a weird problem
when I am trying to destroy active record it is giving this error.
AbstractController::ActionNotFound at /photos/6
The action 'destroy' could not be found for PhotosController
here is what my controller look like
class PhotosController < ApplicationController
before_filter :authenticate_user!
....
....
def delete
#photo = current_user.photos.find(params[:id])
#photo.destroy
redirect_to photos_path
end
and if I perform the same actions using console it is working fine(the record is getting deleted successfully ).
This is my routes file look like.
resources :photos, :only => [:index, :show, :new, :create] do
post 'upload', :on => :collection
end
I know I have not included :destroy in resources please suggest me how to insert :destroy action to delete photos
if you have resource of photo then action name should be destroy, not delete. and if not then please check your routes.
Seven default action that are generated by scaffolding are follow
index
new
edit
create
update
show
destroy # not delete
I am assuming you are using resources :photo.
The action should be destroy and not delete, as per the Rais Guide.
The seven default actions are: index, new, create, show, edit, update and destroy
def destroy
#photo = current_user.photos.find(params[:id])
#photo.destroy
redirect_to photos_path
end
You can also see the available routes by:
rake routes
EDIT
The problem is here: :only => [:index, :show, :new, :create], which is saying to rails: do not create destroy, edit or update routes.
To solve the problem, you can add destroy to it :only => [:index, :show, :new, :create, :destroy] or use :except instead: :except => [:edit, :update]
If you don't want to limitate the resources:
resources :photos do
post 'upload', :on => :collection
end
EDIT 2 - I don't really understand why you are trying to use delete instead of destroy, However, if you have a good reason for it:
resources :photos :only => [:index, :show, :new, :create] do
post 'upload', :on => :collection
get 'delete', :on => :member
end
In this way you will have the delete_photo_path, which can be used in your show view:
<%= link_to 'Delete', delete_photo_path(#photo) %>
Finally, the action delete should looks like:
def delete
#photo = Photo.find(params[:id])
#photo.destroy
redirect_to photos_path
end

RoutingError with Nested Resources and Destroy

I'm in a sort of weird situation where I'm getting a strange error with nested resources.
I have a nested resource defined as below:
resources :users do
resources :comments, :only => [:create, :destroy]
end
My end point for comments is json only so its controller is defined as follows. Take note that I am using cancan and actsAsApi gems.
class CommentsController < ApplicationController
load_and_authorize_resource
self.responder = ActsAsApi::Responder
respond_to :json
# POST /comments.json
def create
flash[:notice] = 'Comment was successfully created.' if #comment.save
respond_with(#comment, :api_template => :default)
end
# DELETE /comments/1.json
def destroy
#comment.destroy
respond_with(#comment, :api_template => :default)
end
I can then send a post request to '/users/1/comments.json' with some request parameters and the comment will get created like expected. Unfortunately I am getting an error where it tries to locate the destroy action:
Completed 404 Not Found in 169ms
ActionController::RoutingError (No route matches {:action=>"destroy", :controller=>"comments", :id=>#<Comment id: 34, user_id: 1, text: "test test test", created_at: "2012-02-28 06:45:49", updated_at: "2012-02-28 06:45:49">}):
app/controllers/comments_controller.rb:12:in `create'
As extra information, if I modify routes.rb to this:
resources :comments, :only => [:destroy]
resources :users do
resources :comments, :only => [:create]
end
I don't see any error.
Because you are using nested resources you need to tell cancan to load both the users and the comments for actions on comments to work.
See as follows:
class CommentsController < ApplicationController
load_and_authorize_resource :user
load_and_authorize_resource :comment, :through => :user
end
See more details on the cancan nested resource page
I have been able to figure this out. Basically it is required that when you nest resources you use respond_with as follows:
respond_with(#comment.note, #comment, :api_template => :default)

Resources