Project

the_grid

0.0
No commit activity in last 3 years
No release in over 3 years
Provides API for building response based on ActiveRecord::Relation objects (json, csv, even using custom view builder). It makes much easier to fetch information from database for displaying it for example using JavaScript MV* based frameworks (such as Knockout, Backbone, Angular, etc), in csv format or even with any custom format. Tags: json, csv, grid, api, grid builder, activerecord relation builder, relation
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 1.0.0
~> 2.13

Runtime

 Project Readme

Yet Another Grid

This plugin is designed to provide API for building response based on ActiveRecord::Relation objects (json, csv, even using custom view builder). It makes much easier to fetch information from database for displaying it using JavaScript MV* based frameworks such as Knockout, Backbone, Angular, etc.

Getting started

First of all specify grid in your Gemfile and run bundle install. After gem is installed you need to run rails generate the_grid:install. This will generate grid initializer file with basic configuration.

Usage

Controller:

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  respond_to :json

  def index
    @articles = Article.published
    respond_with @articles
  end
end

View:

# app/views/articles/index.json.grid_builder
grid_for @articles, :per_page => 25 do
  searchable_columns :title

  column :title
  column(:url)        { |a| article_url(a) }
  column(:created_at) { |a| a.created_at.to_s(:date) }
  column(:author)     { |a| a.author.full_name }
end

The same grid defenition can be used with different formats.

API

The API is based on commands. Term command describes client's action which can be simple or complicated as well. The general request looks like:

http://your.domain.com/route.json? with_meta=1 &
  page=1 &
  cmd[]=sort & field=title & order=desc &
  cmd[]=search & query=test &
  cmd[]=filter & filters[created_at][from]=1363513288 & filters[created_at][to]=1363513288

Each parameter relates to options of a command. The only exception is with_meta parameter which is used to retrieve extra meta information of grid.

Commands

There are 2 types of commands: batch commands (e.g. update, remove) and select commands (e.g. search, paginate, sort, filter). Select commands can be processed per one request (i.e. stacked) by TheGrid::Builder (method execute_on of such commands always returns ActiveRecord::Relation). Batch commands can't be processed by TheGrid::Builder even more they are ignored (method execute_on returns array of processed records or boolean value). There are few predefined commands: paginate, search, sort, filter, batch_update, batch_remove.

Paginate

This command has 2 non-required parameters:

  • page specifies page of data (integer number, starts with 1)
  • per_page specifies how much records should be selected per one page (integer number)

Sort

This command has also 2 paramters:

  • field specifies sort column (string)
  • order specifies sort order (asc or desc)

Filter

This command requires only one hash parameter filters but it can be in 3 different forms:

  • { :title => "test" } => name = "test"

  • { :created_at => { :from => ... , :to => ..., :type => "time|date|nil" } } => created_at >= :from AND created_at <= :to

    • type specifies type of from/to parameters (optional, can be date or time). If :type is date from/to fields will be parsed into dates using to_time method. If :type is time from/to fields should be timestamps.
    • from/to specifies top and bottom limits (one of them can be omitted)
  • { :id => [1, 2, 3] } => id IN (1,2,3)

Search

This command requires only one parameter query which specifies search string.

Batch Update

This command requires one parameter items - an array of hashes (with stringified keys). Each hash should contain integer value with key id. Hash row is ignored if id is omitted or non-integer.

Batch Remove

This command also requires one parameter item_ids - array of integer ids. Value of array is ignored if it's non-integer.

Run batch commands

It's impossible to run batch commands using TheGrid::Builder. By default command is considered as batch one if its class name starts with Batch. If you want to override this behavior you need to implement instance method batch?. So, client has to manually build grid instance and call run_command! method:

TheGrid.build_for(Article).run_command!('batch_update', params)

Actually it's possible to run any command as shown in line above. Example of controller's batch action:

class ArticlesController < ApplicationController
  def batch_update
    articles = TheGrid.build_for(Article).run_command!('batch_update', :items => params[:articles])
    render :json => build_grid_response_for(articles, :success => "Articles has been successfully updated")
  rescue ArgumentError => e
    render :json => { :message => e.message, :status => :error }
  end

private
  def build_grid_response_for(records, options = {})
    error_message = records.select(&:invalid?).map{ |r| r.errors.full_messages }.join('. ')
    if error_message.blank?
      {:status => :success, :message => options[:success]}
    else
      {:status => :error, :message => error_message}
    end
  end
 end

Create/override commands

It's a normal situation when client needs a custom command or a custom version of existing command. Suppose there is a need in suspend command which change status of records into suspended. Command class should implement at least 2 methods: configure and run_on (if command is a batch one it should implement batch? method which returs true). configure method should return validated parameters or raise an error if one of the required options is missed. Example:

module GridCommands
  class BatchSuspend < TheGrid::Api::Command
    def configure(relation, params)
      ids = params[:item_ids].reject{ |id| id.to_i <= 0 }
      raise ArgumentError, "There is nothing to update" if ids.blank?
      { :item_ids => ids }
    end

    def run_on(relation, params)
      relation.where(relation.table.primary_key.in(params[:item_ids])).update_all(:status => 'suspended')
    end
  end
end

For running this command it's also necessarely to update commands_lookup_scopes. It can be done inside grid intializer file:

# config/initializers/grid.rb
TheGrid.configure do |config|
  # Specifies scopes for custom commands
  config.commands_lookup_scopes += %w{ grid_commands }
  # ....
end

Then it will be possible to run:

TheGrid.build_for(Article).run_command!('batch_suspend', :item_ids => params[:id])

Using lookup technique it's possible to override existing commands. Suppose there is a need to customize batch_update command to allow non-integer ids:

module GridCommands
  class BatchUpdate < TheGrid::Api::Command::BatchUpdate
    def configure(relation, params)
      items = params[:items].reject{ |item| item['id'].to_s.strip.blank? }
      raise ArgumentError, "There is nothing to update" if items.blank?
      { :items => items }
    end
  end
end

Template Builder

For Rails based application there is a template builder which does all the stuff under the hood.

# app/views/articles/index.json.grid_builder
grid_for @articles, :per_page => 2 do
  column :title
  column :description
end

Such view is converted into the next json response:

{
  "max_page": 3,
  "items": [
    {
      "id": 1,
      "title": "My test article",
      "description": "Something interesting"
    },
    {
      "id": 2,
      "title": "My hidden article",
      "description": "Something not interesting"
    }
  ]
}

It's possible to format column output by passing block into column declaration:

# app/views/articles/index.json.grid_builder
grid_for @articles, :per_page => 2 do
  column :title
  column :description
  column(:created_at){ |article| article.created_at.to_s(:date) }
end

Also it's possible to specify extra information for each column (e.g. editable, searchable, etc):

# app/views/articles/index.json.grid_builder
grid_for @articles, :per_page => 2 do
  column :title, :editable => true, :sortable => true, :an_option => "any extra information"
  column :description, :editable => true
  column(:created_at, :editable => true){ |article| article.created_at.to_s(:date) }
end

Looks like a mess, don't it? However there are helper's methods which helps to clean up this view:

# app/views/articles/index.json.grid_builder
grid_for @articles, :per_page => 2 do
  editable_columns :title, :description, :created_at
  sortable_columns :title

  column :title, :an_option => "any extra information"
  column :description
  column(:created_at){ |article| article.created_at.to_s(:date) }
end

It's possible to specify any features for columns using the next DSL method template: "#{feature}ble_columns" (e.g. visible_columns *columns_list). searchable_columns method is a bit special. It not only marks column with searchable flag but also specifies which columns will be searched when search command is run.

Sometimes it's reasonable to add extra meta information into response:

grid_for @articles, :per_page => 2 do
  searchable_columns :title, :created_at
  editable_columns :title

  # specify any kind of meta parameter
  server_time Time.now
  my_option   "Something important for Frontend side"

  column :title
  column(:created_at){ |r| r.created_at.to_s(:date) }
end

Columns meta and extra meta information will be accessible in response only if client specifies non-empty with_meta parameter in request. The previous example is converted into:

{
  "meta": {
    "server_time": "2013-03-17 02:11:05 +0200",
    "my_option": "Something important for Frontend side"
  },
  "columns": [
    {
      "name": "title",
      "searchable": true,
      "editable": true
    },
    {
      "name": "created_at",
      "searchable": true
    }
  ],
  "max_page": 3,
  "items": [
    {
      "id": 1,
      "title": "My test article",
      "created_at": "03/17/2013"
    },
    {
      "id": 2,
      "title": "My hidden article",
      "created_at": "03/16/2013"
    }
  ]
}

per_page option can be omitted. In such cases will be used params[:per_page] or default per page value specified inside grid initializer. Sometimes client need to retrieve all records without pagination. So, for disabling pagination just set per_page option to false. In such cases max_page will be omitted in response.

Nested scopes and tree-like structures

If you need to create tree-like stucture for custom grid view (e.g. complex navigation) you can use scope_for declaration:

grid_for @groups, :per_page => 2 do
  column :name
  column :is_active do |p|
    params[:current_id].to_i == p.id
  end

  scope_for :articles do
    column :title
    column :created_at do |a|
      a.created_at.to_s(:date)
    end
  end
end

This example builds the next response:

{
  "max_page": 2,
  "items": [
    {
      "id": 1,
      "name": "test",
      "is_active": true,
      "articles": [
        {
          "id": 2,
          "title": "Something inetresting",
          "created_at": "03/17/2013"
        },
        {
          "id": 4,
          "title": "test article",
          "created_at": "03/14/2013"
        }
      ]
    },
    {
      "id": 3,
      "name": "test2",
      "is_active": false,
      "articles": [
        {
          "id": 3,
          "title": "test article 2",
          "created_at": "03/13/2013"
        }
      ]
    }
  ]
}

If you need to standardize output you can specify :as option - the column name for nested grid (e.g. if you specify :as => :children then articles key will be substituted with children key). Also there are 2 conditional options :unless and :if which accepts lambda or symbol. If you specify symbol as condition will be used column value with such name (in this case it's important that column is defined before scope). If you need some custom logic to detect if scope should be created for such row or not you can pass lambda.

For example we want to get articles only of active/current group:

grid_for @groups, :per_page => 2 do
  column :name
  column :is_active do |p|
    params[:current_id].to_i == p.id
  end

  scope_for :articles, :as => :children, :if => :is_active do
    column :title
  end
end

Or the same with lambda:

grid_for @groups, :per_page => 2 do
  column :name
  column :is_active do |p|
    params[:current_id].to_i == p.id
  end

  scope_for :articles, :as => :children, :if => lambda{ |group| group.id == params[:current_id].to_i } do
    column :title
  end
end

This produces the response:

{
  "max_page": 2,
  "items": [
    {
      "id": 1,
      "name": "test",
      "is_active": true,
      "children": [
        {
          "id": 2,
          "title": "Something inetresting",
          "created_at": "03/17/2013"
        },
        {
          "id": 4,
          "title": "test article",
          "created_at": "03/14/2013"
        }
      ]
    },
    {
      "id": 3,
      "name": "test2",
      "is_active": false,
      "children": null
    }
  ]
}

Command delegation

Sometimes there is a need to delegate command processing to nested nested grid. For example, there are groups and articles. You need to display groups sorted by name asc and provide ability to sort articles inside each group by any columns. For such purposes you can use delegate declaration:

grid_for @groups, :per_page => 2 do
  delegate :sort => :articles, :filter => :articles

  column :name
  column :is_active do |p|
    params[:current_id].to_i == p.id
  end

  scope_for :articles, :as => :children, :if => :is_active do
    column :title
  end
end

CSV builder

It's possible to generate csv using grid views. The main power is that this view builder also responds to also the api request parameters except pagination. So, it's possible to filter records for output csv. All what you need is just create a simple view:

# app/views/products/index.csv.grid_builder
grid_for @products do
  column :title
  column :qty
  column :created_at
end

And in your controller

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  respond_to :csv, :html

  def index
    @products = Product.active
    respond_with @products
  end
end

Then you can send requests like:

http://your.domain.com/route.csv?cmd[]=filter&filters[is_active]=1

and will get csv file which contains only filtered active products.

License

Released under the MIT License