Nesting resources within a singular resource - ruby-on-rails

Given the following routes:
resource :public_profile do
resources :posts
end
resource :private_profile do
resources :posts
end
How can I, in the PostsController, determine which singular resource I am nested within?

One way you could do this is by creating 2 more controllers that extend some main PostsController, and use
resource :public_profile do
resources :posts, controller: "PublicPostsController"
end
resource :private_profile do
resources :posts, controller: "PrivatePostsController"
end
You could even do this in a variety of ways. For example, maybe it makes sense to have
class ProfileController < ApplicationController; end
class PostsController < ApplicationController; end
class Private::ProfileController < ProfileController; end
class Private::PostsController < PostsController; end
class Public::ProfileController < ProfileController; end
class Public::PostsController < PostsController; end
with routing
resource :public_profile, controller: "Public::ProfileController" do
resources :posts, controller: "Public::PostsController"
end
resource :private_profile, controller: "Private::ProfileController" do
resources :posts, controller: "Private::PostsController"
end
Regardless of how you set this up, you can easily 'know' what resource you're nested within because you'll actually be running within a separate controller specific to that nesting and can thus have a perfect place for logic specific to that nesting. For general logic, you'd put that into the parent PostsController.
Another way you could do this is by adding a before_filter to PostsController like
before_filter :check_nesting
private
def check_nesting
#is_public_profile = params.include?(:public)
end
and have routing like
resource :public_profile, public: true do
resources :posts, controller: "PublicPostsController"
end
resource :private_profile, private: true do
resources :posts, controller: "PrivatePostsController"
end
I don't care for this approach though.

You can route them to different controllers ( by specifying it in the routes) , that are extended from the same "base" controller PostsController. In the extended controllers you
identify them:
EX:
resource :public_profile do
resources :posts, :controller => "public_profile_posts_controller"
end
resource :private_profile do
resources :posts, :controller => "private_profile_posts_controller"
end
and the Controllers
class PublicProfilePostsController < PostsController
before_filter :identify_controller
def identify_controller
#nested_resource_of = :public_profile
end
end
class PrivateProfilePostsController < PostsController
before_filter :identify_controller
def identify_controller
#nested_resource_of = :private_profile
end
end
and then you have access to the variable
#nested_resource_of
in the PostsController actions

Related

Adding a concern to the controller causes AbstractController::ActionNotFound

I use the same set_modpack method in several controllers, and I decided to make a concern for it. But when I add the concern to the controller, all routes give me AbstractController::ActionNotFound error.
My routes:
concern :commentable do
resources :comments, shallow: true
end
resources :modpacks, concerns: :commentable do
resources :releases, controller: 'modpack_releases'
end
My concern:
# app/controllers/concerns/modpack_derivative.rb
module ModpackDerivative
extend ActiveSupport::Concern
protected
def set_modpack
#modpack = Modpack.find(params[:id])
if #modpack.nil?
#modpack = Modpack.find_by!(slug: params[:id])
end
end
end
My controller:
# app/controllers/modpacks_controllers.rb
class ModpacksController < ApplicationController
include ModpackDerivative
before_action :set_modpack, only: [:show, :update, :destroy]
# 100% standard index, create, show,
# update and destroy actions (API)
private
def modpack_params
params.require(:modpack).permit(:logo, :name, :slug, :summary, :tags, :description)
end
end
It works fine if I delete the concern and paste the set_modpack methods inside the controllers.

Is there a clean way to find a polymorphic instance of an object in a nested controller?

I'm working with a nested controller using models with a polymorphic relations. I was wondering if there is a clean way to to find #holder. The code in CommentsController#set_holder is ugly and I was wondering if rails provides helpers for this issue.
class Comment < ActiveRecord::Base
belongs_to :holder, polymorphic: true
end
class Product < ActiveRecord::Base
has_many :comments, as: :holder
end
class User < ActiveRecord::Base
has_many :comments, as: :holder
end
Dummy::Application.routes.draw do
resources :products do
resources :comments
end
resources :users do
resources :comments
end
end
class CommentsController < ApplicationController
before_action :set_holder, only: [:new, :create]
# code ...
def new
#comment = #holder.comments.build
end
# code ...
private
def set_holder
# params = {"controller"=>"comments", "action"=>"new", "user_id"=>"3"}
# or
# params = {"controller"=>"comments", "action"=>"new", "product_id"=>"3"}
# Is there a Rails way to set #holder?
type, type_id = params.find { |k,_| /_id$/ === k }
#holder = type.sub(/_id$/, '').classify.constantize.find(type_id)
end
end
You can try with:
resource, id = request.path.split('/')[1, 2]
#holder = resource.classify.constantize.find(id)
Also in routes you can make things shorter by:
resources :users, :products do
resources :comments
end

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.

Routing for sub ressources in rails

So this is in routes file :
resources :users do
resources :lamps
end
I want to be able to display a user's "lamps" with something like :
http://localhost:3000/users/2/lamps
and show all the existing lamps whichever user owns it using :
http://localhost:3000/lamps
both are very different as the first one is more of a management view and the later is more what a user browsing would see.
The thing is they both go to the index action of the lamp_controller
How can I manage this in a clean way without having to use if statements in my action/view?
You can use the module option to route the nested routes to a namespaced controller:
Rails.application.routes.draw do
resources :users do
resources :lamps, only: [:index], module: 'users'
end
resources :lamps, only: [:index]
end
Another way to do this for a group of resources is:
Rails.application.routes.draw do
resources :users do
scope module: 'users' do
resources :lamps, only: [:index]
resources :chairs
resources :rugs
end
end
resources :lamps, only: [:index]
end
This would let you handle the different contexts in separate controllers:
# app/controllers/lamps_controller.rb
class LampsController < ApplicationController
# GET /lamps
def index
#lamps = Lamp.all
end
end
# app/controllers/users/lamps_controller.rb
class Users::LampsController < ApplicationController
before_action :set_user
# GET /users/:user_id/lamps
def index
#lamps = #user.lamps
# renders /views/users/lamps/index.html.erb
end
private
def set_user
#user = User.find(params[:user_id])
end
end

Route concern and polymorphic model: how to share controller and views?

Given the routes:
Example::Application.routes.draw do
concern :commentable do
resources :comments
end
resources :articles, concerns: :commentable
resources :forums do
resources :forum_topics, concerns: :commentable
end
end
And the model:
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
When I edit or add a comment, I need to go back to the "commentable" object. I have the following issues, though:
1) The redirect_to in the comments_controller.rb would be different depending on the parent object
2) The references on the views would differ as well
= simple_form_for comment do |form|
Is there a practical way to share views and controllers for this comment resource?
In Rails 4 you can pass options to concerns. So if you do this:
# routes.rb
concern :commentable do |options|
resources :comments, options
end
resources :articles do
concerns :commentable, commentable_type: 'Article'
end
Then when you rake routes, you will see you get a route like
POST /articles/:id/comments, {commentable_type: 'Article'}
That will override anything the request tries to set to keep it secure. Then in your CommentsController:
# comments_controller.rb
class CommentsController < ApplicationController
before_filter :set_commentable, only: [:index, :create]
def create
#comment = Comment.create!(commentable: #commentable)
respond_with #comment
end
private
def set_commentable
commentable_id = params["#{params[:commentable_type].underscore}_id"]
#commentable = params[:commentable_type].constantize.find(commentable_id)
end
end
One way to test such a controller with rspec is:
require 'rails_helper'
describe CommentsController do
let(:article) { create(:article) }
[:article].each do |commentable|
it "creates comments for #{commentable.to_s.pluralize} " do
obj = send(commentable)
options = {}
options["#{commentable.to_s}_id"] = obj.id
options["commentable_type".to_sym] = commentable.to_s.camelize
options[:comment] = attributes_for(:comment)
post :create, options
expect(obj.comments).to eq [Comment.all.last]
end
end
end
You can find the parent in a before filter like this:
comments_controller.rb
before_filter: find_parent
def find_parent
params.each do |name, value|
if name =~ /(.+)_id$/
#parent = $1.classify.constantize.find(value)
end
end
end
Now you can redirect or do whatever you please depending on the parent type.
For example in a view:
= simple_form_for [#parent, comment] do |form|
Or in a controller
comments_controller.rb
redirect_to #parent # redirect to the show page of the commentable.

Resources