Abhi On Rails

Ruby On Rails - What I learned recently

Active Admin - Custom Pages, Views and Actions

Active Admin is a Ruby on Rails plugin to generate Admin console for the application. It has a lots of functionalities implemented out of the box where we just need to integrate it and use it. Most of the time you don’t need to customize the plugin.

But the main Advantage of Active Admin is that you can customize almost all the built-in functionalities. The main functionalities are: Navigation, User Authentication, Scopes, Action Items, Index styles, Filters, Sidebar sections, API and Downloads.

By default Active Admin will generate the pages to CRUD a model when you generate the resource file for the model using rails generator:

    $> rails generate active_admin:resource [MyModelName]
  

But what if in your application you need an Admin page where you want to show some calculated data of a model or multiple models which is obviously not a resource file. You can create custom pages:

      # app/admin/calendar.rb
      ActiveAdmin.register_page "Calendar" do
        content do
          render partial: 'calendar', :locals => {:transactions => Transaction.all}
        end
      end
    

Now you can put your view/html content in the partial:

# App/views/admin/calendar/_calendar.html.arb <table> <tr> <th>Date</th> <th>From</th> </tr> <% transactions.each do |transaction| %> <tr> <td><%= transaction[:date] %></td> <td><%= transaction[:type] %></td> </tr> <% end %> </table> <%= paginate transactions %>

This is enough to create a simple custom page which will show the data in a customized view. But in some situation you need a button to perform some operations like download the data. In those situations you need custom action and action item. Modified custom page:

      # app/admin/calendar.rb
      ActiveAdmin.register_page "Calendar" do
        content do
          render partial: 'calendar', :locals => {:transactions => Transaction.all}
        end
        
        page_action :csv, method: :get do
          transactions = Transaction.all.page(params[:page]).per(Transaction::ADMIN_PAGE_LIMIT)
          csv = CSV.generate( encoding: 'Windows-1251' ) do |csv|
            # add headers
            csv << [ "Date", "From"]
            transactions.each do |transaction|
              csv << [ transaction[:date], transaction[:type]]
            end     
          end
          # send file to user
          send_data csv.encode('Windows-1251'), type: 'text/csv; charset=windows-1251; header=present', disposition: "attachment; filename=transactions_#{DateTime.now.to_s}.csv"
        end
        
        action_item :add do
          link_to "Export to CSV", admin_calendar_csv_path, method: :get
        end
      end
    

You can also add custom filters to filter the data. You just need to add the following line to the custom page:

      sidebar :filter, partial: 'filter'
    

and create a view for the filter partial

# app/views/admin/calendar/_filter.html.arb <%= form_tag("/admin/calendar", method: "get", class: "filter_form") do %> <div class="filter_form_field"> <%= label_tag(:type, "From") %> <%= select_tag(:type, options_for_select([['All', 'all'], ['Type1', 'type1'], ['Type2', 'type2']], params[:type])) %> </div> <div class="filter_form_field"> <%= label_tag(:date, "Date") %> <%= date_field_tag(:date, params[:date]) %> </div> <div class="buttons"> <input type="submit" name="commit" value="Filter"> <a class="clear_filters_btn" href="/admin/Calendar">Clear Filters</a> </div> <% end %>

You need to use the filter params in your custom page to filter the data:

      Transaction.where(:type => /.*#{params[:type]}.*/) 
    

As I mentioned before, this is the greatest advantage of using Active Admin. You can customize almost all the functionalities :)

comments...

Integrating FriendlyID, Inherited Reource and Active Admin

For my latest spare time project Kazhakkoottam I had to integrate friendly id with the app. But the application was already integrated and running with Inherited Resource and Active Admin. So while integrating with friendly id I have faced some issues.

So why we need to use friendly id?
FriendlyId is used to implement pretty URL's, so that it will be SEO friendly and can be identified easily. Instead of id's you can configure name or title field as the slug. For example http://www.kazhakkoottam.com/items/krishna-theatre is the friendly id for http://www.kazhakkoottam.com/items/1.

Integration issue with Inherited Resource
The first issue I faced was in the show page of an Item in ItemsController. ActiveRecord::RecordNotFound Couldn't find Item with id=lorem-ipsum. The reason is Inherited resource was using Item.find instead of Item.friendly.find()
Added the following code to the ItemsController to fix this issue:

    # app/controllers/items_controller.rb
    defaults resource_class: Item.friendly
  

Integration issue with Active Admin
Then I faced the same issue with Active Admin while accessed the show page of the item from Admin console. The solution was to add the following code to the active admin file for item.

      # app/admin/item.rb
      controller do
        def find_resource
          scoped_collection.friendly.find(params[:id])
        end
      end
    

Now I got the pretty URL integrated with my app which is SEO friendly and can be identified easily.

comments...

Active Admin tips and tricks

For my current project I had to setup Admin console. I used Active Admin for that purpose. It is very easy to setup an Admin console. And they have a very good documentation. But if you want make some customization you have to spent some time digging for solution. Following are some customization I made to improve the Admin console:

1.member_label

If you are using select box for showing association in form page or filters, it will show the object string if there is no attribute 'name' for that model. By default the select box will take the name attribute of the model as the values. So you have to use member_label to specify the attribute you want to show in the select box.
f.input :user, :as => :select, :member_label => :email
  

2.Custom Menu

I had to modify the menus by grouping some menus to sub-menus. I have added the following code to initializers/active_admin.rb:
 admin.build_menu do |menu|
   menu.add :label => "Product", :priority => 2
 end
  

This will create a main menu 'Product'. To add sub-menus you have to mention this menu as the parent.
 menu :parent => "Product", :priority => 3
  

Here priority is the order in which we need to show the sub-menu.

3.Custom Actions

For one model Admin doesn't have the privilege to create new record. So I have to remove the new action from the Admin console. So I added the following code:
actions :all, :except => [:new]
  

4.Columns in show page

For one of my model, in the show page I felt there are lots of space available in the right side. But to show something in the right side I had to customize the show page. So I used columns to show multiple associated data in the show page:
show do |product_request|
  columns do
    column do
      attributes_table do
        row :id
        row :user
      end
    end

    column do
      active_admin_comments
    end
  end
    
  columns do
    column do
      h3 "Businesses"
      table_for resource.businesses do 
        column :id
        column :business
      end
    end

    column do
      h3 "Products"
      table_for resource.products do 
        column :id
        column :product
      end
    end
  end
end
  

5.Link to filter of another model

For one of the models (For Eg. User) I had to show a link in the index page to show list of associated data (For Eg. Business) ie., I have to show the index page of business with a filter of user.
column "Businesses" do |user|
  link_to "Click", :controller => "businesses", 
                   :action => "index", 'q[user_id_eq]' => "#{user.id}".html_safe
end
  

6.Implementing Authorization

In one of my project I had implemented both User model (for consumers) and Admin User model (for Active Admin). But later more and more roles got added to the Consumer user and we had to implement same set of features in both Active Admin and the user side. Some of the User roles like "Managers" need some of the reports which were already present for Admin.
So I decided to merge User and AdminUser models. Keep only User model as it has more entries than AdminUser model. To implement this we need to make only a small change in the Active Admin initializer.
    # config/initializers/active_admin.rb
    ActiveAdmin.setup do |config|
    ...
    config.current_user_method = :current_user
    config.logout_link_path = :destroy_user_session_path
    # config.skip_before_filter :authenticate_user!
    ...
    end
  

The next step was to show/hide the Active Admin menus based on the roles. It was pretty much simple, just need to implement the role checking functions in the User model and use that in the corresponding Active Admin resource pages.
    # app/admin/post.rb
    ActiveAdmin.register Post do
    menu :if => proc{ current_user.manager? }
    ...
    end
  

comments...