Routing: Combining resources and nested resources - ruby-on-rails

/items should list all items
/user/items should list only items for the current user
I have defined a relationship between users and items, so current_user.items works fine.
My question is:
How does the items#index action know whether it has been called directly or via a user resource in order to know what to populate #items with?
# routes.rb
resources :items
resource :user do
resources :items
end
# items_controller.rb
class ItemsController < ApplicationController
def index
#items = Item.all
end
end
# users_controller.rb
class UsersController < ApplicationController
def show
#user = current_user
end
end

It looks hacky, but it was my first idea :)
resource :user do
resources :items, :i_am_nested_resource => true
end
class ItemsController < ApplicationController
def index
if params[:i_am_nested_resource]
#items = current_user.items
else
#items = Item.all
end
end
end
Or you can go brute way: parse your request url :).

I solved this by separating the code into two controllers (one namespaced). I think this is cleaner than adding conditional logic to a single controller.
# routes.rb
resources :items
namespace 'user' do
resources :items
end
# items_controller.rb
class ItemsController < ApplicationController
def index
#items = Item.all
end
end
# user/items_controller.rb
class User::ItemsController < ApplicationController
def index
#items = current_user.items
end
end

Related

Rails: How to redirect to a specific controller (index) depending on condition

I have a Ruby on Rails application that can generate "roles" for actors in movies; the idea is that if a user looks at a movie detail page, they can click "add role", and the same if they look at an actor detail page.
Once the role is generated, I would like to redirect back to where they came from - the movie detail page or the actor detail page... so in the controller's "create" and "update" method, the redirect_to should either be movie_path(id) or actor_path(id). How do I keep the "origin" persistent, i. e. how do I remember whether the user came from movie detail or from actor detail (and the id, respectively)?
I would setup separate nested routes and just use inheritance, mixins and partials to avoid duplication:
resources :movies do
resources :roles, module: :movies, only: :create
end
resources :actors do
resources :roles, module: :actors, only: :create
end
class RolesController < ApplicationController
before_action :set_parent
def create
#role = #parent.roles.create(role_params)
if #role.save
redirect_to #parent
else
render :new
end
end
private
# guesses the name based on the module nesting
# assumes that you are using Rails 6+
# see https://stackoverflow.com/questions/133357/how-do-you-find-the-namespace-module-name-programmatically-in-ruby-on-rails
def parent_class
module_parent.name.singularize.constantize
end
def set_parent
parent_class.find(param_key)
end
def param_key
parent_class.model_name.param_key + "_id"
end
def role_params
params.require(:role)
.permit(:foo, :bar, :baz)
end
end
module Movies
class RolesController < ::RolesController
end
end
module Actors
class RolesController < ::RolesController
end
end
# roles/_form.html.erb
<%= form_with(model: [parent, role]) do |form| %>
# ...
<% 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.

Admin privileges in Pundit with nested resources and how to handle the index action

I'm new to pundit and trying to come up with the best approach for handling nested resources for the index action. I found a similar question however it doesn't deal with admin privileges and I'm just not sure if my solution feels quite right.
Let's say I have two models, a User can have many notes and a Note which belongs to a single user. Users cannot look at notes from other users unless they're an admin. At the same time, admin's are able to create their own notes and therefore must also have the ability to retrieve a list of them via their own index action.
routes.rb
resources :users, only: :show do
resources :notes
end
notes_controller.rb
class NotesController < ApplicationController
#would probably move to application_controller.rb
after_action :verify_authorized
after_action :verify_policy_scoped
def index
user = User.find(params[:user_id])
#notes = policy_scope(user.notes)
authorize user
end
#additional code
end
note_policy.rb
class NotePolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin? && scope != user.notes
scope
else
user.notes
end
end
end
#additional code
end
user_policy.rb
class UserPolicy < ApplicationPolicy
def index?
user == record || user.admin?
end
#additional code
end
You are overthinking it:
class NotePolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.where(user: user)
end
end
def index?
record == user || user.admin?
end
# ...
end
Note here that its a good idea to chain from the scope being passed in from policy_scope. It lets your controller set up any scopes unrelated to authorization like for example pagination.
Also in index? we are cheating slightly. Instead of passing a note instance we are just passing the user.
class NotesController < ApplicationController
before_action :set_user!, only: [:index] # ...
before_action :set_note!, only: [:show, :edit, :update, :destroy]
def index
#notes = policy_scope(Note.all)
authorize(#user)
end
# ...
private
def set_user!
#user = User.find(params[:user_id])
end
def set_note!
#note = authorize( Note.find(params[:id]) )
end
end
Using before_action in this way is a pretty good pattern as it sets up all the "member" actions for authorization.

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.

Nested (and Unested at-same-time) Resources better approach

I have two models like that
class Plan < ActiveRecord::Base
belongs_to :profile
And
class Profile < ActiveRecord::Base
has_many :plans
And routes like: (I need to)
resources :profiles do
resources :plans
end
resources :plans
So, following up ruby-on-rails - Problem with Nested Resources, I've made my PLANS index controller like this, to works NESTED and UNESTED at same time (the only way I've found for now):
def index
if params.has_key? :profile_id
#profile = Profile.find(params[:profile_id])
#plans = #profile.plans
else
#plans = Plan.all
end
There is a cleaner approach to this?
I have another models in this situation, and putting all actions, in all controllers to behave like this is cumbersome.
You gave me an idea:
models/user.rb:
class User < ActiveRecord::Base
has_many :posts
attr_accessible :name
end
models/post.rb:
class Post < ActiveRecord::Base
belongs_to :user
attr_accessible :title, :user_id
end
controllers/posts_controller.rb:
class PostsController < ApplicationController
belongs_to :user # creates belongs_to_user filter
# #posts = Post.all # managed by belongs_to_user filter
# GET /posts
# GET /posts.json
def index
respond_to do |format|
format.html # index.html.erb
format.json { render json: #posts }
end
end
end
And now the substance:
controllers/application_controller.rb:
class ApplicationController < ActionController::Base
protect_from_forgery
def self.belongs_to(model)
# Example: model == :user
filter_method_name = :"belongs_to_#{model}_index" # :belongs_to_user_index
foreign_key = "#{model}_id" # 'user_id'
model_class = model.to_s.classify # User
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{filter_method_name} # def belongs_to_user_index
if params.has_key? :'#{foreign_key}' # if params.has_key? :user_id
instance_variable_set :"##{model}", # instance_variable_set :"#user",
#{model_class}.find(params[:'#{foreign_key}']) # User.find(params[:user_id])
instance_variable_set :"#\#{controller_name}", # instance_variable_set :"##{controller_name}",
##{model}.send(controller_name.pluralize) # #user.send(controller_name.pluralize)
else # else
instance_variable_set :"#\#{controller_name}", # instance_variable_set :"##{controller_name}",
controller_name.classify.constantize.all # controller_name.classify.constantize.all
end # end
end # end
EOV
before_filter filter_method_name, only: :index # before_filter :belongs_to_user_index, only: :index
end
end
The code is not complex to understand if you have notions of Ruby metaprogramming: it declares a before_filter which declares the instance variables inferring the names from the controller name and from the association. It is implemented just for the index actions, which is the only action using the plural instance variable version, but it should be easy to write a filter version for the other actions.

Resources