Custom action route inheritance in Rails - ruby-on-rails

I have the following controller inheritance chain (note that models do NOT inherit, but rather have a parent-child relationship): EmployeesController < SitesController < CompaniesController
I have an action, companies#dashboard that the subclasses inherit. Right now this is my routing:
get "/company/dashboard(/:disguised_id)" => "companies#dashboard"
get "/site/dashboard(/:disguised_id)" => "sites#dashboard"
get "/employee/dashboard(/:disguised_id)" => "employees#dashboard"
But I'm wondering if there's a more elegant way to set up this inheritance chain for a custom action?

You can use routing concerns to reuse sets of routes:
Rails.application.routes.draw do
concern :dashboard do
get '/dashboard(/:disguised_id)', action: :dashboard
end
scope :company, controller: :companies do
concerns :dashboard
end
scope :site, controller: :sites do
concerns :dashboard
end
scope :employee, controller: :employees do
concerns :dashboard
end
end
Concerns can be included in scope, namespace, resource and resources. A better solution here would be to use namespaces together with a separate dashboard controller:
Rails.application.routes.draw do
concern :dashboard do
get '/dashboard(/:disguised_id)', to: 'dashboard#show'
end
namespace :companies, path: 'company' do
concerns :dashboard
end
namespace :sites, path: 'site' do
concerns :dashboard
end
namespace :employees, path: 'employee' do
concerns :dashboard
end
end
This lets both lets you offload a responsibility from the normal controller which is already responsible for CRUD'ing the resource and use inheritance:
class DashboardController < ApplicationController
def show
# do something awesome
end
end
module Companies
class DashboardController < ::DashboardController
end
end
module Sites
class DashboardController < ::DashboardController
end
end
module Employees
class DashboardController < ::DashboardController
end
end

I guess what you would like to do, is just DRY the routes, isn't it?
You might do that easier by having the only route for 3 resources. Let's say it would be "pages#dashboard". So you need the pages controller with dashboard. I use pages controller for landing pages, faqs, dashboard and e.t.c. So these are static routes that usually never changes. Because I won't delete dashboard, landing page, or FAQ section.
Then in dashboard you might render company_dashboard, site_dashboard or employee_dashboard depending on the resource.
And finally, you might have the only path in routes or do it like:
get "/dashboard", to: "pages#dashboard"
Of course, you might create another controller "dashboard" with "index" and serve the logic up there. But because I don't know your code or future design ideas, I might be wrong by advising the most easiest solution for me. But what I mean, is that probably you don't need 3 different controllers for dashboard.

Related

Rails - Clean way to mix nested+non-nested or member routes?

New to programming in general and Rails specifically.
TLDR:
Is there a clean way of using a variety of member actions or controlling flow in controllers for nested resources? Is there a third option? I have read through Rails Routing from the Outside In and related stackoverflow posts, and haven't reached a satisfactory conclusion, so any help is appreciated.
Situation:
I would like for my users to be able to see all locations, and to see all locations in a specified group. (Locations also have groups, users have groups, etc).
Initially, I defined the routes and actions:
resources :groups do
member do
get :locations
end
end
resources :locations do
member do
get :groups
end
end
This worked fine, but I also need users to be able to create location in a specific group, etc.
I could define more member routes, e.g.
resources :groups do
member do
get :locations_index
get :location_new
end
end
resources :locations do
...
end
I have considered replacing the above with mixed nested and non-nested resources:
resources :groups do
resources :locations
end
resources :locations
etc.
The problem is that I am concerned the controllers will need a lot of flow control to ensure users see what I want them to see, can create locations in groups or not in groups, and so on.
Using before filters would help, but my controllers would still be much fatter than I'd like.
So, as above, is there a clean way of doing this?
You can use the module option to route the nested resources to a separate controller:
resources :locations, only: [:index]
resources :groups do
resources :locations,
only: [:index],
module: :groups
end
class LocationsController < ApplicationController
# Displays all locations
# GET /locations
def index
#locations = Location.all
end
end
module Groups
class LocationsController < ApplicationController
before_action :set_group
# Displays the locations of a group
# GET /groups/1/locations
def index
#locations = #group.locations
end
private
def set_group
#group = Group.find(params[:group_id])
end
end
end
It can be passed to most of the routing macros such as resource/resources, scope, namespace, member and collection.
This keeps the controllers lean since they are only handling a single representation of a resource.
The problem is that I am concerned the controllers will need a lot of flow control to ensure users see what I want them to see, can create locations in groups or not in groups, and so on.
Thats really the job of a separate authentication layer such as Pundit.
Try to avoid non REST routes.
You can have a look to this blog post http://jeromedalbert.com/how-dhh-organizes-his-rails-controllers/

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

Rails, Devise: Same resource, different controller based on user type

I have a resource, say Product, which can be accessed by two different user classes: say Customer and Admin. There is no inheritance between these two.
I am using Devise for authentication:
# config/routes.rb
devises_for :customers
deviser_for :admins
I have these two controllers:
# app/controllers/customers/products_controller.rb
class Customers::ProductsController < ApplicationController
and
# app/controllers/admins/products_controller.rb
class Admins::ProductsController < ApplicationController
Now depending on who logs in (Customer or Admin), I want products_path to point to the corresponding controller. And I want to avoid having customers_products_path and admins_products_path, that's messy.
So I have setup my routes as such
# config/routes.rb
devise_scope :admin do
resources :products, module: 'admins'
end
devise_scope :customer do
resources :products, module: 'customers'
end
This doesn't work. When I login as a Customer, products_path still points to Admins::ProductsController#index as it is the first defined.
Any clue? What I want to do might simply be impossible without hacking.
UPDATE
According to the code, it is not doable.
It turns out the best way to achieve that is to use routing constraints as such:
# config/routes.rb
resources :products, module: 'customers', constraints: CustomersConstraint.new
resources :products, module: 'admins', constraints: AdminsConstraint.new
# app/helpers/customers_constraint.rb
class CustomersConstraint
def matches? request
!!request.env["warden"].user(:customer)
end
end
# app/helpers/admins_constraint.rb
class AdminsConstraint
def matches? request
!!request.env["warden"].user(:admin)
end
end
I stored the constraint objects in the helper folder because I don't really know the best place to put them.
Thanks to #crackofdusk for the tip.
For this you will need to override the existing after_sign_in_path_for method of devise. Put this in your app/controllers/application_controller.rb
def after_sign_in_path_for(resource)
if resource.class == Admin
'/your/path/for/admins'
elsif resource.class == Customer
'/your/path/for/customers'
end
end
Note: If you want to implement previous_url that was requested by user then you can use it like this:
session[:previous_url] || 'your/path/for/admins'

Namespace, static pages, and inherited controllers, what files in what folders?

I'm building an admin control panel (attempting to ;) ).
I have been looking at Backend administration in Ruby on Rails and as suggested I am trying to make Admin::AdminController that checks for admin and sets the layout etc.
But I'm also trying to set a route in it that routes /admin to /admin/dash
From my understanding of reading http://guides.rubyonrails.org/routing.html#controller-namespaces-and-routing , specifically section 2.6,
Admin::AdminController
tells rails that Admin is the name space, AdminController is the controller which is a subclass (extension?, implementation of the interface?) of ApplicationController. Which would imply the controller should live in app/controllers/admin/ and be called admin_controller.rb.
But what I want is
AdminController
I get errors like:
uninitialized constant Admin::Controller
My code for the route:
match :admin, :to => 'admin/admin#dash'
namespace :admin do
# Directs to /admin/resources/*
match '/dash', to: '#dash'
resources :users, :pictures
end
I have put the controller in app/controllers/admin, app/controllers and the combinations with
class Admin::AdminController < ApplicationController
before_filter :admin_user
# / ** STATIC ADMIN PAGES ** /
def dash
end
end
or class AdminController < ApplicationController.
Edit: Maybe it's my understanding of routing.
Example:
namespace :admin do
get "/dash"
vs.
namespace :admin do
match "/dash" to "admin#dash"
vs.
namespace...
match "/dash" to "#dash"
The first one makes it so i can display a dash via the controller, i.e. admin/dash would be controlled by
AdminController < ApplicationControler
def dash
end
Does the second one route admin/admin/dash to admin/dash?
TL/DR:
I think my confusion comes from syntax or my poor understanding of RESTful practices or maybe even class / object inheritance in ruby.
Thanks for helping this n00b out. :)
Side question: can I change my code to be minimized until someone opens it like a spoiler so it doesn't crowd things up if I find more information and add it?
I think your initial approach was correct, but you need to change it a little.
1) insert the /admin => /admin/dash inside the namespace (imho, its better to redirect it)
match 'admin' => redirect('admin/dash')
or
namespace :admin do
match '/', to: 'admin#dash'
end
2) You can't match '/dash' to '#dash' since you're not inside a resource block, you're inside a namespace block, so it doesnt' have implied controller.
namespace :admin do
match 'dash', to: 'admin#dash'
# This will go to Admin::AdminController#dash
# (first Admin because of the namespace,
# and the second because of the controller name)
end
hope it works :D
What you want is "scope" in routing.
scope "/admin" do
resources :articles
end
Which will route /admin/articles to ArticlesController (without Admin:: prefix)
Documentation covers almost every possible case.
http://edgeguides.rubyonrails.org/routing.html

In Rails, how to have an /admin section, and then controllers within the admin section?

I want to have a /admin section in my application, and have routes within this /admin section like:
www.example.com/admin/ (only certain users have acess to this section)
then have controllers in this section like:
/admin/users/{add, new, etc}
What are my options for something like this? (using rails 3)
I prefer to do something similar to Todd's answer but slightly different. Rather than adding the before_filter to each controller related to Admin stuff I prefer to create an AdminController that all controllers related to admin actions can inherit from:
# config/routes.rb
namespace :admin do
resources :users
end
# app/controllers/admin_controller.rb
class AdminController < ApplicationController
before_filter :authorized?
private
def authorized?
unless current_user.has_role? :admin
flash[:error] = "You are not authorized to view that page."
redirect_to root_path
end
end
end
# app/controllers/admin/users_controller.rb
class Admin::UsersController < AdminController
...
end
Do something like this in your routes.rb:
namespace :admin do
resources :users
end
See http://guides.rubyonrails.org/routing.html for more detail.
Then in each admin controller you'll need a before_filter:
before_filter :authorized?
def authorized?
#check if authorized here.
end
As Todd mentioned, you want to add a namespaced route:
namespace :admin do
resources :users
end
You also need to put your controllers, views, etc in subfolders of each of these sections called "admin/". If you're generating this from scratch, it's easy:
rails g controller admin/users
This may seem pretty complicated, but I have an article that walks through all of this, with a sample rails 3 app you can download to play around with it:
Routing in Ruby on Rails 3
Then in each admin controller you'll need a before_filter:
before_filter :authorized?
def authorized?
#check if authorized here.
end
I think it's better if he puts this code into a main AdminController which inherits from ApplicationController, then each admin controller will inherits from this AdminController.
About Rails3, here is a good article about routes
Obviously what Todd said is correct. However if you're a fan of additional security through obscurity, you can also keep your new_admin_user url helpers and Admin:: namespaced controllers, but provide a less widely-used public url path with the following:
scope :module => "admin", :as => 'admin', :path => 'xyz' do
resources :user
end
A rake route with that setup will show routes along these lines:
new_admin_user GET /xyz/users/new(.:format) {:controller=>"admin/users", :action=>"new"}
I suppose the only actor this would thwart is an unsophisticated attacker who's crawled and compiled a bunch of Rails sites that provide system access at admin/, but I don't see any harm in daring to be different with your admin console paths really.
application_controller.rb
before_filter :if_namespace_is_admin?
def if_name_space_is_admin?
#now you should check to see if the namespace is from admin
#now you need namespaces because ruby ns confuse the f'out of me
end

Resources