views:

445

answers:

4

Say there is a product controller which you want to have an index (list products) action. Easy. Now say you have an admin and store parts in your project. Both need to list products, but in a slightly different manner (store's one shouldn't have this edit product link for instance). They also use different layouts.

So far my idea is to have two product controllers under different namespaces - app/controllers/admin/products_controller.rb and app/controllers/store/products_controller.rb - each then having its own views and layouts. But I suspect this may lead to WET code. Or to references to other controller views (which imo breaks modularity and hence should be avoided).

So, the actual question: is there a more DRY (or in fact proper) way to achieve the above?

I'm not sure the title actually reflects the question. But, on the other hand, if it were, I could probably google the answer.

A: 

You're describing the Model-View-Controller Pattern, in which models views and controllers can vary orthogonally (or more or less orthogonally, depending on how its implemented).

Very basically, you have one View that allows edits and one that doesn't. Again, depending on implementation, the editable view may derived form the uneditable view. In either case, either the controller or some higher-level code will conditionally chose the right view.

tpdi
Even in the non-edit view, you'd need some intelligence in the view to determine whether to display the edit link -- unless you just want it to fail for non-admins. I'd prefer that it not be visible. So unless you want to have completely separate views for admin/normal user as he describes, you have to introduce a little more coupling between the controller and view.
tvanfosson
I don't understand why a non-edit view should ever display a edit link. For users with edit rights, the controller or dispatcher should select the editable view, and that view should just unconditionally show the edit link,
tpdi
+1  A: 

If the way you're displaying products between the admin section and the store section is constant except for the admin links (Create, Edit, Destroy), then I think it would be easiest to create a partial for your product. I assume you have a way of telling whether the user is an admin or not (I'll just use admin? for simplicity below). Inside your partial you do something like this...

<div class="product">
    <div class="productheader">
        <%=h product.title %>
    </div>
    <div class="productdescription>
        <%=h product.description %>
    </div>
    <% if admin? %>
    <div class="productadmin">
        <%= link_to "Delete", destroy_product_url %>
        <%= link_to "Edit", edit_product_url %>
    </div>
    <% end %>
</div>

Be sure to name this partial _product.html.erb (the underscore tells rails that the template is a partial). Create a folder in the app/views directory of your application called shared and store the partial there.

To render this partial in your other views, simply call the render method and pass the partial parameter.

A single product:

<%= render(:partial => "shared/product", :object => @a_product) %>

Multiple products:

<%= render(:partial => "shared/product", :collection => @products) %>

Layouts can be applied to partials by adding the layout parameter. Partial layouts must be prefixed with the underscore but stored in the app/views directory associated with the controller.

<%= render(:partial => "shared/product", :object => @a_product, :layout => "somelayout" %>
Jason Punyon
+1  A: 

The approach that I take is to have a single controller for products and add code to it to detect the role that the user plays and conditionally set view data based on that role. This includes both actual model data and data used only by the view to determine which bits of the interface to display. The view itself, then, contains some small amount of code that is able to act on the role-based data and render only those bits that are relevant to the particular role. One might argue that this is injecting either some small bit of business logic into the view or some small bit of display logic into the controller -- and those arguments have some validity. However, I find that it's really more of a balancing act between principles and I prefer value DRY over MVC-purity.

tvanfosson
A: 

If only there were some kind of view inheritance... So that one can subclass controller without need to supply all its views. Good thing is that there is this patch. Bad thing is that it can't make it to the core for quite a while.

Having applied it to my rails 2.2, I managed to have the following answer to the original question.

Subclassing controller

ProductController has been blessed with the twins:

class Products::AdminController < ProductsController
  layout 'admin'
  before_filter :authenticate
end

and

class Products::StoreController < ProductsController
  layout 'store'
  before_filter :find_cart
end

This itself looks quite nice since each of them as well carries its own initialization part.

Changing routes

  map.resources :products, :controller => 'products/admin', :path_prefix => 'admin',
    :name_prefix => 'admin_'
  map.resources :products, :controller => 'products/store', :path_prefix => 'store',
    :only => [:show, :index], :name_prefix => 'store_'

Not an easy route, defo. But, hey, after this point everything just works (assuming you fixed path helpers) with ProductController views and partials.

Shared views changes

Each subclass controller has its own version of index.html.erb. Everything else is shared in a base class.

Speaking about path helpers in shared templates. What used to be

<% form_for @product ... %>

becomes

<% form_for [controller_name, @product] ... %>

and thins like

<%= link_to products_path %>

turn into

<%= link_to send("#{controller_name}_products_path") %>

I don't know if it is all worth it, but that is a way. Anyone knows why if there are plans to include this patch in rails soon?

artemave