How to organize controller in moderately large Rails application? - ruby-on-rails

I'm working on an application with quite a few related models and would like to hear some opinions on how best to organize the controllers.
Here are some options I have been considering:
1) Namespace the controllers. So, for example, have a controllers/admin directory and a controllers/public directory. This seems appealing for organization but also sorta contrived since a single resource could often have actions which might sensibly belong in different directories (e.g. the show action is public whereas the create action is admin). So this would mean breaking up some of my resources into two separate controllers - one public, one admin. Seems bad.
2) Create nested resources. I've only occasionally used nested resources so it's not always clear to me when it's best to nest resources versus simply passing the data you need through the params explicitly. Does anyone has some suggestions/examples of how best to use nested resources? When is it a good idea? When is it an overkill?
3) Just leave the default scaffolded controllers alone. Create new collection/member actions where needed and use before filters to set permissions within each controller. This seems most appealing since it keeps things simple upfront. But I'm sorta nervous about things getting messy down the line as some of the controllers might start to bloat with several new actions.
If anyone with experience designing large applications could offer some guidance here, it'd greatly appreciated.

For organizing within our applications, I have done a little bit of everything depending on the situation.
First, regarding the separate controllers for admin/user functions, I will say that you probably don't want to go that route. We used authorization and before_filter to manage rights within the application. We rolled our own, but 20/20 hind-sight, we should have use CanCan. From there you can setup something like this (this is pseudo-code, actual language would depend on how you implemented authorization):
before_filter :can_edit_user, :only => [:new, :create, :edit, :update] #or :except => [:index, :show]
protected
def can_edit_user
redirect_to never_never_land_path unless current_user.has_rights?('edit_user')
end
Or at a higher level
before_filter :require_admin, :only [:new, :create]
and in your application controller
def require_admin
redirect_to never_never_land_path unless current_user.administrator?
end
It would depend which route, but I would use that for authorization instead of splitting up controllers.
As far as name spaces vs. Nested Resources, it depends on the situation. In several of our apps, we have both. We use name spaces when there is a cause for a logical separation or there will be shared functions between a group of controllers. Case and point for us is we put administrative functions within a namespace, and within we have users, roles and other proprietary admin functions.
map.namespace :admin do |admin|
admin.resources :users
admin.resources :roles
end
and then within those controllers we have a base controller, that stores our shared functions.
class Admin::Base < ApplicationController
before_filter :require_admin
end
class Admin::UsersController < Admin::Base
def new
....
end
This provides us logical separation of data and the ability to dry up our code a bit by sharing things like the before_filter.
We use nested controllers if there is going to be a section of code where you want some things to persist between controllers. The case from our application is our customers. We search for and load a customer and then within that customer, they have orders, tickets, locations. Within that area we have the customer loaded while we look at the different tabs.
map.resources :customers do |customer|
customer.resources :tickets
customer.resources :orders
customer.resources :locations
end
and that gives us urls:
customers/:id
customers/:customer_id/orders/:id
customers/:customer_id/tickets/:id
Other advantages we have experienced from this is ease of setting up menu systems and tabs. These structures lend themselves well to an organized site.
I hope this helps!

Also, looks like nesting resources more than one level deep is almost certainly a bad idea:
http://weblog.jamisbuck.org/2007/2/5/nesting-resources
Yeah, it's a old article, but it makes a lot of sense to me.
If anyone disagrees, I'd like to hear why.

Related

Rails 4 custom admin backend

I'm wanting to create an admin backend for a practice app. After reading around I've come to a general idea of what to do but would like some clarification:
Use namespace to route the backend at example.com/admin...
Put any adminifiable resources inside namespace e.g resources :posts
At this point do I duplicate the normal (public facing) controllers and put them into the admin directory? (Along with CRUD views)
In theory the public facing controllers only need index and show actions, right? As new/create/update/destroy will only be accessed in the admin/controller.
Any clarification or advice is greatly appreciated. Just trying to wrap my head around this.
I would recommend against duplicating your controllers, or any part of your application for that matter. That goes entirely against the DRY principle, which stands for "Don't Repeat Yourself." Duplicate code becomes really hard to maintain and test as your application grows.
Instead, I would recommend limiting access to certain actions using before filters. For example, let's say that you want users to be able to create posts, read posts and see listings of posts. In your PostsController, you can have something like this:
before_action :admin_user?, only: [:edit, :destroy]
Note: before_action is just the new name for before_filters.
So then actions like index would execute normally for all users, but if a user calls the destroy action, the controller would first check to see if the user is an admin, by calling an admin_user? method (typically defined in ApplicationController). This method could be a simple conditional, like "if the user is not an admin, flash an error message and redirect them back to where they were before the request" and then use it to protect any action or resource you want. You could also use it in the views to show delete buttons on posts only if the user is an admin, for instance.
That's for resource-specific actions. Often times it's also a good idea to have a section of the site that consolidates resource views and administrative actions. This would be its own controller/view (I call mine AdminController) and you can protect all actions in it with the above method:
before_action :admin_user?
To make your resources available to AdminController using the methods defined inside the individual resource controllers, you can do this in routes.rb:
namespace :admin do
resources :users
end
This will make it so that http://yoursite.com/admin/users/index will still call the index action in the Users controller, but it will happen within the context of an admin user (because of the before_action above).

A good approach to different indexes of same resource in Rails?

Something I've been wondering about is the best approach to creating a different index view of the same resource in Rails.
For example, let's say you have a group of Users, and the current Index action of the Users Controller that generates a tabular view of the users. You then want to create a second Index view that plotted all the users on a map. Would the best approach be to create a new action in the Users controller, or to create a new controller (like UsersMap) with a new index?
Thoughts appreciated...
My route file looks like this:
resources :users, :only => [:index, :index_with_map, :show, :edit, :update] do
match '/index_with_map' => 'users#index_with_map'
end
But hitting "users/index_with_map" results in:
ActiveRecord::RecordNotFound at /admin/users/index_with_map Couldn't find User with id=index_with_map
You could add that action to the UsersController, but if you are going to have more actions including Users and Maps, you might wanna think of making a new controller (to conform to the Single Responsibility Principle).
From the guides:
If you find yourself adding many extra actions to a resourceful route,
it’s time to stop and ask yourself whether you’re disguising the
presence of another resource.
You could do this:
resources :users do
collection do
get 'index_with_map'
end
end
This will enable Rails to recognize a path such as /users/index_with_map with GET.
The best approach would be to add new action that will deal with the users map. Then you will only need to create the corresponding view file for that action.
The users are for the same table, the functionality is only different. So why the need of different controller?
Also if you will create the another controller just for map view then there is no use of that as it's just a wastage of resources.
Another point is -
Controllers provides the user to interact with the model. So two controllers means you typically want two models. So different controllers are used when you want to do different (categories of) things.

Namespacing Controllers & nesting Resources

Is it a terrible idea to use both Name-spacing and nested resources?
I want to have an admin area, with a bunch of controllers in them. Some resources in that area would make sense to be nested for example:
resources :suppliers do
resources :products
resources :locations
end
Whilst namespacing like this:
map.namespace :admin do |admin|
resources :suppliers do
resources :products
resources :locations
end
end
Is it possible / a good idea to use nesting within a namespace like this? How should i structure things?
Namespacing an admin area is a good idea as it keeps those controllers separated from your public/user facing controllers. The big win here is security since your admin actions are likely to be capable of doing more and may bypass certain security restrictions like removing or limiting the amount of authorization, depending on how you want to structure your administration access.
As for nesting resources, use it if it makes sense. If you never want to access one of the nested resources outside of the context of it's parent resource, then using nested resources is a good option.
As an example, if your admin interface was to be accessed by suppliers, and each admin was to be scoped to their resources only, then it might make authorization simpler to nest the resources since you can simply query through that nested resource and your authorization is simplified to checking that their account is tied to that supplier.
class Admin::ProductsController < AdminController
before_filter :load_supplier
# your actions
def load_supplier
# Will trigger a 404 if the supplier does not belong to the admin
#supplier = current_admin.suppliers.find(params[:supplier_id])
end
end
Of course it really depends on what your trying to accomplish, what is the expected audience of the admin area, will they have full access to everything. If so will they need access to resources outside of the context of any relationships. For example what if I'm an admin and I need to do some search/sort/filter on all of the products, regardless of the supplier (or maybe filtering by one or more suppliers), and then generate CSV/Excel from those constraints. In this case using nested resources might make this difficult or impossible.
I've personally found that nested resources make more sense in user/public facing controllers and to be more annoying in admin areas, but then I've always built admin interfaces that were limited to few people. In which case I typically turn to a gem like ActiveAdmin which basically gives you a full CRUD on each model with plenty of customization options.

Rails ACL Design Question

I am working on a legacy rails codebase and need to implement some ACL logic that is not based around standard CRUD operations on objects, but instead around custom logic about what parts of a page are shown to different groups of users. These page "parts" are not defined by any objects in the database, so my guess is that object-based ACL systems may not be the best fit here.
I currently researching declarative_authorization but haven't yet determined if it can do anything other than object-based permissions.
This must be a rather common use-case for web applications and I'd prefer not to roll yet another ACL. Does anyone have a suggestion for a 3rd party library that would work well for this?
I think CanCan can help you. It's simple to use and should do just what you need. Here's a RailsCast: Authorization with CanCan
I have looked through a whole bunch of different Authorization and ACL plugins and like you I did not like the CRUD approach that most of them used.
In the end, the most suitable for me was a setup similar to that which Redmine uses. I don't know if it origins from any named plugin but I spent some time getting to understand it and made the necessary adjustments for my case.
Basically what it does is to allow use of controllers and actions, either global or for specific models. You start with specifying what different permissions are assignable:
MyApplication::ACL.mapper do |map|
map.permission :view_project, {:projects => :show}
map.permission :manage_project, {:projects => [:update, :edit, :post_status]}
map.permission :delete_project, {:projects => :destroy}
end
Next step is to assign one or more permissions to a Role which is a model with a serialized field which can store the permissons, ie:
<#Role id: 1 name: "Intern" permissions: [:view_project] >
<#Role id: 2 name: "Member" permissions [:view_project, :manage_project] >
And then you map the different Roles with Users by a Membership model. The Membership can also be mapped with specific models, like Project, since you might be allowed to manage one project but not another one, or it can be global like some index actions which should be restricted but you don't know which Model to authorize against yet.
In the controllers you verify all this by using:
before_filter :authorize, :only => [:show, :update, :edit,
:post_status, :destroy]
And authorize is of course a method that checks if the current user is a member of any role that has the permission required for the current Controller and Action.
This is, although a long description here, only in short how it works :) It is alot more complicated than other ACL plugins out there but I think it is the most "clean" alternative while still allowing the flexibility that I need.

Static pages in Rails?

So I'm wondering what the best way to do static pages in Rails is, or rather Rails 3. I've always been a little confused about this, like should I create a controller or not?
There are a variety of approaches you can take (they're not all good approaches).
1 - public directory
If it's truly static, you can put it in the public directory. Things in the public directory will be served immediately, without going through the Rails stack.
Advantages:
Because it doesn't have to waste time going through the Rails stack, the client will receive a faster response.
Disadvantages:
You won't be able to use your sites layout. Or view helpers like link_to.
Instead of your views being in one place (app/views), now they're in two places (app/views and public). This can be confusing.
Thoughts: I feel pretty strongly that the disadvantages outweigh the advantages here. If you're looking to make a minor improvement in speed at the expense of readability and programmer happiness, why use Rails in the first place?
2 - Place in app/views and render directly from the Router
It is possible to render views from the router. However, it definitely isn't The Rails Way.
From the official RailsGuide on routing:
1 The Purpose of the Rails Router
The Rails router recognizes URLs and dispatches them to a controller's action.
Architecturally, there isn't anything inherently wrong with having a router map directly to a view. Many other frameworks do just that. However, Rails does not do that, and deviating from an established convention is likely to confuse other developers.
like should I create a controller or not?
Unless you want to take one of the approaches mentioned above - yes, you should create a controller.
The question then becomes what to name the controller. This answer outlines some options. I'll list them here with some thoughts. I'll also add three other options.
3 - Use ApplicationController
# routes.rb
get "/about" to: "application#about"
# application_controller.rb
class ApplicationController < ActionController::Base
def about
render "/about"
end
end
# app/views/about.html.erb
The advantage here is that you don't introduce overhead/bloat by creating a new controller and folder. The disadvantage is that it's not The Rails Way. Every controller you create inherits from ApplicationController. ApplicationController is typically used to house functionality that you want to share between all other controllers. See this example from the Action Controller Overview Rails Guide:
class ApplicationController < ActionController::Base
before_action :require_login
private
def require_login
unless logged_in?
flash[:error] = "You must be logged in to access this section"
redirect_to new_login_url # halts request cycle
end
end
end
4 - StaticController or StaticPagesController
Michael Hartl's popular Ruby on Rails Tutorial uses a StaticPagesController. I agree with the source I got this from in that I don't like this approach because the pages often aren't actually static.
Also, there is a possibility of confusion - why did we put other static views in separate controllers? Shouldn't static views be rendered from the StaticPagesController? I don't think the possibility of confusion is too high, but still wanted to note it.
Also note Hartl's footnote:
Our method for making static pages is probably the simplest, but it’s not the only way. The optimal method really depends on your needs; if you expect a large number of static pages, using a Static Pages controller can get quite cumbersome, but in our sample app we’ll only need a few. If you do need a lot of static pages, take a look at the high_voltage gem. ↑
5 - PagesController
The official Ruby on Rails routing guide uses PagesController. I think this approach is fine, but it isn't descriptive at all. Everything is a page. What distinguishes these pages from the other pages?
6 - UncategorizedPagesController
I would call the controller UncategorizedPagesController, because that's exactly what they are - uncategorized pages. Yes, it's a little more cumbersome to type and read. I prefer the advantage of clarity over conciseness, but I could understand the choice to be more concise and go with PagesController, or something else.
7 - High Voltage gem
With High Voltage, you don't have to do the tedious work of writing out routes and empty controller actions:
# routes.rb
root 'pages#home'
get '/about', to: 'pages#about'
get '/contact', to: 'pages#contact'
get '/help', to: 'pages#help'
get '/terms-of-service', to: 'pages#terms_of_service'
get '/landing-page', to: 'pages#landing_page'
...
# pages_controller.rb
def PagesController < ApplicationController
def home
end
def about
end
def contact
end
def help
end
def terms_of_service
end
def landing_page
end
...
end
You just add your pages to app/views/pages and link to them: <%= link_to 'About', page_path('about') %>.
It depends on if they're truly static. You can always add pages to the public/ directory of your app, and they'll work just fine. They don't even fire up rails or touch the routing engine.
However, most pages on the site, including static, still need to use the site's layout. You don't want to have to update the layout in dozens of pages separately. In this case, you can create a "catchall" controller. Here's an example:
rails g controller site home about_us location
Then you can put the page-specific content in app/views/site/home.html.erb, for instance.
UPDATE: You can go a step further and cache those pages with a call to caches_page near the top of your controller:
class SiteController < ApplicationController
caches_page :home, :about_us, :location
end
Just be aware that if you have dynamic page elements, like a list of links that changes based on whether the user is logged in, page caching isn't going to work for you. But this info should get you pointed in the right direction.
If they're 100% static, just shove them into public.
For example, when you spin up a new rails project, there's an index.html in your public.
If you want nicer routing, then yeah, creating a controller probably wouldn't be a bad idea.
I prefer to create a controller. Good tutorial and explanation on static pages:
http://railstutorial.org/chapters/static-pages#top

Resources