All threads / Non Restful actions in the controller

Ask A Question

Notifications

You’re not receiving notifications from this thread.

Non Restful actions in the controller

Jay Killeen asked in Rails

I originally asked this question about building a reporting feature in my app https://gorails.com/forum/advice-on-building-a-reports-feature.

I'd like to simplify this question even further. I want to create custom views that my user can go to for certain models in my database. For example, I have a model called 'Project' and project has a veiw for index and show. So I can show a list of all the projects in the system, scoped using Pundit so the user only sees those projects they have the authority to see. The user can then click the project and go to its 'show' page. I then add a tab in my navbar called Projects which takes them to their index.

Now Projects can be assigned to individual users and a user can edit that project and mark it is 'active'. Meaning, 'I am assigned this project but I have a lot of projects so I want to mark this one as active because I am working on it'. Now the user goes away, navigates around my app, then wants to quickly click a link that takes them to their active projects.

Do I do this on the 'index' page? Do I create a new action on the controller called 'active projects' that renders to a view called 'active projects'? Can I place a scope on the model called active and then somehow make it so the index passed active as the parameter on the index? Essentially I could have heaps of these different scopes, active projects, inactive projects? I have a ransack search box that allows the user to do heaps of searches but it is difficult to use.

How would you guys implement this simple feature?

I tried this

  def show
    if params[:id] == "dashboard"
      @projects = policy_scope(Project)
      render :dashboard
      return
    end
    authorize @project
  end

But ran into issues with my Pundit authorization before_filters (what I'd like to put is what I have written below but obviously you can't do that).

  after_filter :verify_authorized,  except: [:index, :show(params[:id] == dashboard)]
  after_filter :verify_policy_scoped, only: :index

I mean, I could do all this by making a whole new controller, route and just using the existing models and authorization but it just seems like such a simple thing to do.

Been looking around and it seems I may be able to do this on the index action instead of the show. Is there a way I can pass a value into the params on the index action?? So I could pass dashboard to the index action and then render a different view than the index.html.erb??

http://stackoverflow.com/a/9779820/2585189

My two suggestions would to either put this in the index or in an action called active with it's own route.

  1. You create the active action with a route. This will let you create a separate view and render a different HTML template.

  2. You just add it to the index by checking a query param:

def index
  @projects = policy_scope(Project)
  @projects = @projects.active if params[:filter] == "active"
end

Lots of different ways you could implement this, but the simplest example is just adding a scope to the Relation that you are rendering based upon a param like filter. This is nice for adding more filters in the future which you might want to do as well.

Okie dokie, so I thought long and hard about this one and have come up with a pattern that suits me. I have separated out the idea into two features 'custom views' and 'dashboards'. Dashboards are more complex and I will just run these with their own controller, routes and models. 'Custom Views' is for more specific model related views that I want the users to have quick access to. So what I have done is:

I'll make the example on my customers model.

  1. Created a new route that gets hit before the customer resources (so it doesn't conflict with the customer show route).
  resources :customer_custom_views, :controller => "customers/custom_views", only: [:index, :show]
  resources :customers, only: [:index, :show, :search] do
    collection do
      match 'search' => 'customers#index', via: [:get], as: :search
    end
  end
  1. Created a controller nested inside a folder called customers -controllers --customers ---customer_views_controller.rb
class Customers::CustomViewsController < ApplicationController

  before_action :authenticate_user!
  before_action :redirect_blocked_user

  def index
    @customer_custom_views = built_custom_views
  end

  def show
    @id = params[:id]
    case @id
      when 'owned'
        @customers = policy_scope(current_user.customers).paginate(:page => params[:page])
      when 'active'
        @customers = policy_scope(Customer.active).paginate(:page => params[:page])
      when 'active_blocked'
        @customers = policy_scope(Customer.active_blocked).paginate(:page => params[:page])
    end
    direct_to_built_custom_view
  end

  private

    def built_custom_views
      built_custom_views = ['owned', 'active', 'active_blocked']
    end

    def direct_to_built_custom_view
      if built_custom_views.include?(@id)
        verify_policy_scoped
        render "show_#{@id}"
      else
        flash[:error] = 'That is not a valid custom view for customers'
        redirect_to customer_custom_views_path
      end
    end
end
  1. Created the views in a nested folder inside the customer views

    -views
    --customers
    ---custom_views
    ----index.html.erb
    ----show_active.html_erb
    ----show_active_blocked.html.erb
    ----show_owned.html.erb
    
  2. Added the links to the navbar with

<ul class="dropdown-menu" role="menu">
  <li><a href="<%= customer_custom_views_path %>">Views</a></li>
  <% if current_user.active_customers.present? %>
    <li><a href="<%= customer_custom_view_path('active') %>">My Active Customers</a></li>
  <% end %>
  <li><a href="<%= customers_path %>">Customers</a></li>
  <li><a href="<%= customer_types_path %>">Customer Types</a></li>
  <li><a href="<%= customer_areas_path %>">Customer Areas</a></li>
</ul>
  1. Created a simple index.html.erb that lists all the different custom view I have templates for (this is hardcoded at the moment so I will figure out how to create an activerecord style hash in the controller that includes the name, descriptions etc.
  <p>Please select from the list below to be directed to a range of different views that relate to customers.</p>
  <div class="table-responsive">
    <table class="table table-hover">
      <tr>
        <th>Name</th>
        <th>Description</th>
        <tr>
          <td><%= link_to 'My Assigned Customers', customer_custom_view_path('owned') %></td>
          <td>View all the customers assigned to you.</td>
        </tr>
        <tr>
          <td><%= link_to 'Active Customers', customer_custom_view_path('active') %></td>
          <td>View all the customers assigned to you or your direct reports that are flagged as active.</td>
        </tr>
        <tr>
          <td><%= link_to 'Active Blocked Customers', customer_custom_view_path('active_blocked') %></td>
          <td>View all the customers assigned to you or your direct reports that are active but blocked from purchasing.</td>
        </tr>
    </table>
  </div>
  1. And show pages for each of those reports such as
<% content_for :title, "My Active Customers" %>

<h2>My Active Customers</h2>

<p>Below are all the customers that are assigned to you or one of your direct reports. It has been filtered for active customers which means the 'flag' field on the project has been marked as 'active'. Not all your customers are active so if you want to see all the customers assigned to you then you should go to <%= link_to "Customers", customers_path %> and search there.</p>

<p>
  <strong>Customer Count</strong>
  <%= @customers.count %>
</p>

<%= render 'shared/pagination', :collection => @customers %>
<%= render "/customers/table" %>
<%= render 'shared/pagination', :collection => @customers %>

I can probably do more to dry this up or make it more dynamic but for now it allows my users to have a page they can go to that relates to that particular table and quickly see bits of information that they want to see. It was the really easy to extend this pattern to other tables like 'projects' or 'prices' etc.

Yay!

If anyone has any ideas on how to improve this please shoot me your ideas. As you can see most of this stuff is just getting a scope of the activerecord collection from the customer.rb.

  scope :active, -> { where(active: true) }
  scope :active_blocked, -> { where(active: true, status: 'Blocked') }
  scope :inactive_blocked, -> { where(active: false, status: 'Blocked') }

The Dashboards would have way more complexity like Top 10 Customers by Net Revenue would need to sum on the sales association then order, then limit, then include other tables etc etc so I will probably create a whole class/model that does all that and has its own controller and routes etc.

This turned out pretty sweet. While you can definitely clean it up, as a first pass, this is simple enough that you can easily refactor it later as it grows.

I think that having your scopes, descriptions, and other metadata organized will be useful. It will help you remove the case statement in your controller and that's going to help a lot by defining everything in one place.

This is good work, especially for a first pass. I'd also try to avoid the case statement in your controller. I mean if it works, then great. But I usually like to put more lengthier logic in the model whenever possible and create a class method. But that's just me.

Join the discussion

Want to stay up-to-date with Ruby on Rails?

Join 33,665+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.

    logo Created with Sketch.

    Ruby on Rails tutorials, guides, and screencasts for web developers learning Ruby, Rails, Javascript, Turbolinks, Stimulus.js, Vue.js, and more. Icons by Icons8

    © 2020 GoRails, LLC. All rights reserved.