the_grid 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/README.md +424 -0
- data/Rakefile +1 -0
- data/lib/generators/the_grid/install/install_generator.rb +11 -0
- data/lib/generators/the_grid/install/templates/the_grid.rb +10 -0
- data/lib/the_grid/api/command/batch_remove.rb +14 -0
- data/lib/the_grid/api/command/batch_update.rb +21 -0
- data/lib/the_grid/api/command/filter.rb +43 -0
- data/lib/the_grid/api/command/paginate.rb +29 -0
- data/lib/the_grid/api/command/search.rb +86 -0
- data/lib/the_grid/api/command/sort.rb +17 -0
- data/lib/the_grid/api/command.rb +48 -0
- data/lib/the_grid/api.rb +52 -0
- data/lib/the_grid/builder/context.rb +96 -0
- data/lib/the_grid/builder/json.rb +37 -0
- data/lib/the_grid/builder.rb +61 -0
- data/lib/the_grid/config.rb +18 -0
- data/lib/the_grid/version.rb +3 -0
- data/lib/the_grid.rb +18 -0
- data/spec/api/command/batch_remove_spec.rb +41 -0
- data/spec/api/command/batch_update_spec.rb +44 -0
- data/spec/api/command/filter_spec.rb +89 -0
- data/spec/api/command/paginate_spec.rb +44 -0
- data/spec/api/command/search_spec.rb +64 -0
- data/spec/api/command/sort_spec.rb +23 -0
- data/spec/api/command_spec.rb +44 -0
- data/spec/api_spec.rb +58 -0
- data/spec/builder/context_spec.rb +182 -0
- data/spec/builder/json_spec.rb +46 -0
- data/spec/config_spec.rb +35 -0
- data/spec/spec_helper.rb +2 -0
- data/the_grid.gemspec +34 -0
- metadata +140 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
module TheGrid
|
2
|
+
class Api::Command
|
3
|
+
|
4
|
+
def self.find(cmd)
|
5
|
+
@@commands ||= {}
|
6
|
+
@@commands[cmd] ||= build(cmd)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.register_lookup_scope(scope)
|
10
|
+
scopes.unshift(scope).uniq!
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.scopes
|
14
|
+
@@scopes ||= ["the_grid/api/command"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.build(cmd)
|
18
|
+
scope = scopes.detect do |scope|
|
19
|
+
"#{scope}/#{cmd}".camelize.constantize rescue nil
|
20
|
+
end
|
21
|
+
raise ArgumentError, %{ Command "#{cmd}" is unknown } if scope.nil?
|
22
|
+
"#{scope}/#{cmd}".camelize.constantize.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def execute_on(relation, params)
|
26
|
+
run_on(relation, configure(relation, params))
|
27
|
+
end
|
28
|
+
|
29
|
+
def batch?
|
30
|
+
@is_batch ||= self.class.name.demodulize.starts_with?('Batch')
|
31
|
+
end
|
32
|
+
|
33
|
+
def contextualize(relation, params)
|
34
|
+
{}
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def run_on(relation, params)
|
40
|
+
raise "Method \"#{inspect}::run_on\" should be implemented by child class"
|
41
|
+
end
|
42
|
+
|
43
|
+
def configure(relation, params)
|
44
|
+
params
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
data/lib/the_grid/api.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module TheGrid
|
2
|
+
class Api
|
3
|
+
attr_reader :relation, :options
|
4
|
+
|
5
|
+
def initialize(relation)
|
6
|
+
@relation = relation
|
7
|
+
@options = { :delegated_commands => {} }
|
8
|
+
end
|
9
|
+
|
10
|
+
def delegate(commands)
|
11
|
+
options[:delegated_commands].merge! commands.stringify_keys
|
12
|
+
end
|
13
|
+
|
14
|
+
def compose!(params)
|
15
|
+
configure(params).fetch(:cmd).each do |cmd|
|
16
|
+
@relation = run_command!(cmd, params) unless command(cmd).batch?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def run_command!(name, params)
|
21
|
+
@options.merge! command(name).contextualize(@relation, params)
|
22
|
+
|
23
|
+
if command_delegated?(name)
|
24
|
+
assoc_name = options[:delegated_commands][name.to_s]
|
25
|
+
assoc = @relation.reflections[assoc_name].klass.scoped
|
26
|
+
@relation.merge command(name).execute_on(assoc, params)
|
27
|
+
else
|
28
|
+
command(name).execute_on(@relation, params)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def command(type)
|
35
|
+
::TheGrid::Api::Command.find(type)
|
36
|
+
end
|
37
|
+
|
38
|
+
def command_delegated?(cmd)
|
39
|
+
options[:delegated_commands].has_key?(cmd.to_s)
|
40
|
+
end
|
41
|
+
|
42
|
+
def configure(params)
|
43
|
+
self.delegate(params[:delegate]) if params[:delegate]
|
44
|
+
params.tap do |o|
|
45
|
+
o[:cmd] = Array.wrap(o[:cmd])
|
46
|
+
o[:cmd].unshift(:paginate) unless params[:per_page] === false
|
47
|
+
o[:cmd].uniq!
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module TheGrid
|
2
|
+
class Builder::Context
|
3
|
+
attr_reader :columns, :options, :scope, :name
|
4
|
+
|
5
|
+
def initialize(options = {}, &dsl)
|
6
|
+
@scope = options.delete(:scope)
|
7
|
+
@options = options
|
8
|
+
@columns = { :id => {:hidden => true} }
|
9
|
+
|
10
|
+
self.instance_eval(&dsl)
|
11
|
+
end
|
12
|
+
|
13
|
+
def column(name, attributes = {}, &block)
|
14
|
+
find_or_build_column(name).tap do |column|
|
15
|
+
column.merge! attributes
|
16
|
+
column[:as] = block if block_given?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def method_missing(method_name, *args, &block)
|
21
|
+
if @scope.respond_to?(method_name)
|
22
|
+
@scope.send(method_name, *args, &block)
|
23
|
+
elsif method_name.to_s.ends_with?("ble_columns")
|
24
|
+
feature = method_name.to_s.chomp("_columns")
|
25
|
+
mark_columns_with(feature.to_sym, args)
|
26
|
+
@options[method_name.to_sym] = args
|
27
|
+
else
|
28
|
+
@options[method_name] = args.size == 1 ? args.first : args
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def scope_for(scope_name, attributes = {}, &block)
|
33
|
+
name = attributes.delete(:as) || scope_name
|
34
|
+
column name, attributes.merge(:as => Builder::Context.new(:scope => scope, &block), :scope_name => scope_name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def visible_columns
|
38
|
+
columns.each_with_object({}) do |column, vc|
|
39
|
+
name, options = column
|
40
|
+
vc[name] = options.except(:as, :if, :unless) unless options[:hidden]
|
41
|
+
vc[name] = options[:as].visible_columns if options[:as].respond_to?(:visible_columns)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def assemble(records)
|
46
|
+
records.map{ |record| assemble_row_for(record) }
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def find_or_build_column(name)
|
52
|
+
@columns[name.to_sym] ||= {}
|
53
|
+
end
|
54
|
+
|
55
|
+
def mark_columns_with(feature, column_names)
|
56
|
+
column_names.each do |name|
|
57
|
+
find_or_build_column(name).store(feature, true)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def assemble_row_for(record)
|
62
|
+
columns.each_with_object({}) do |column, row|
|
63
|
+
name, options = column
|
64
|
+
row[name] = assemble_column_for(record, name, options)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def assemble_column_for(record, name, options)
|
69
|
+
formatter = options[:as]
|
70
|
+
|
71
|
+
if formatter.respond_to?(:call)
|
72
|
+
formatter.call(record)
|
73
|
+
elsif formatter.is_a? Symbol
|
74
|
+
record.send(formatter)
|
75
|
+
elsif formatter.respond_to?(:assemble)
|
76
|
+
formatter.assemble(record.send(options[:scope_name])) if may_assemble?(record, options)
|
77
|
+
else
|
78
|
+
record.send(name)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def may_assemble?(record, options)
|
83
|
+
condition = options[:if] || options[:unless]
|
84
|
+
|
85
|
+
if condition.is_a? Symbol
|
86
|
+
result = assemble_column_for(record, condition, columns[condition])
|
87
|
+
elsif condition.respond_to?(:call)
|
88
|
+
result = condition.call(record)
|
89
|
+
else
|
90
|
+
result = true
|
91
|
+
end
|
92
|
+
options[:unless].present? ? !result : result
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module TheGrid
|
2
|
+
class Builder::Json
|
3
|
+
cattr_accessor :prettify_json
|
4
|
+
attr_reader :api, :context
|
5
|
+
|
6
|
+
def initialize(relation, context)
|
7
|
+
@api = TheGrid::Api.new(relation)
|
8
|
+
@context = context
|
9
|
+
end
|
10
|
+
|
11
|
+
def assemble_with(params)
|
12
|
+
options = params.merge context.options
|
13
|
+
api.compose!(options)
|
14
|
+
stringify as_json_with(options)
|
15
|
+
rescue ArgumentError => error
|
16
|
+
stringify as_json_message('error', error.message)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def stringify(json_hash)
|
22
|
+
self.class.prettify_json ? JSON.pretty_generate(json_hash) : json_hash.to_json
|
23
|
+
end
|
24
|
+
|
25
|
+
def as_json_with(options)
|
26
|
+
{}.tap do |json|
|
27
|
+
json[:meta], json[:columns] = context.options.except(:delegate, :search_over), context.visible_columns if options[:with_meta]
|
28
|
+
json[:max_page] = api.options[:max_page]
|
29
|
+
json[:items] = context.assemble(api.relation)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def as_json_message(status, message)
|
34
|
+
{:status => status, :message => message}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module TheGrid
|
2
|
+
class Builder
|
3
|
+
private_class_method :new
|
4
|
+
|
5
|
+
def self.call(template)
|
6
|
+
source = if template.source.empty?
|
7
|
+
File.read(template.identifier)
|
8
|
+
else
|
9
|
+
template.source
|
10
|
+
end
|
11
|
+
|
12
|
+
%{
|
13
|
+
::TheGrid::Builder.assemble(:view_type => ::TheGrid::Builder::Json, :scope => self) {
|
14
|
+
#{source}
|
15
|
+
}
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.assemble(options, &block)
|
20
|
+
new(options, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(options, &block)
|
24
|
+
options.assert_valid_keys(:scope, :view_type)
|
25
|
+
|
26
|
+
@_scope = options.delete(:scope)
|
27
|
+
@_view_type = options.delete(:view_type)
|
28
|
+
|
29
|
+
copy_instance_variables_from(@_scope) if @_scope
|
30
|
+
self.instance_eval(&block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def grid_for(relation, options = {}, &block)
|
34
|
+
context = Context.new(options.merge(:scope => @_scope), &block)
|
35
|
+
@_view_handler = @_view_type.new(relation, context)
|
36
|
+
end
|
37
|
+
|
38
|
+
def assemble(&block)
|
39
|
+
@_view_handler.assemble_with(@_scope.params, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
def method_missing(name, *args, &block)
|
43
|
+
if @_scope.respond_to?(name)
|
44
|
+
@_scope.send(name, *args, &block)
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_s; assemble;end
|
51
|
+
def to_str; assemble;end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def copy_instance_variables_from(object)
|
56
|
+
vars = object.instance_variables.map(&:to_s)
|
57
|
+
vars.each { |name| instance_variable_set(name.to_sym, object.instance_variable_get(name)) }
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module TheGrid
|
2
|
+
class Config
|
3
|
+
attr_accessor :default_max_per_page, :prettify_json, :commands_lookup_scopes
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
self.commands_lookup_scopes = []
|
7
|
+
self.prettify_json = false
|
8
|
+
end
|
9
|
+
|
10
|
+
def apply
|
11
|
+
self.commands_lookup_scopes.flatten.each{ |s| Api::Command.register_lookup_scope(s) }
|
12
|
+
Api::Command.find(:paginate).default_per_page = self.default_max_per_page
|
13
|
+
Builder::Json.prettify_json = self.prettify_json
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
ActionView::Template.register_template_handler :grid_builder, ::TheGrid::Builder if defined?(ActionView::Template)
|
18
|
+
end
|
data/lib/the_grid.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'the_grid/version'
|
2
|
+
require 'the_grid/api'
|
3
|
+
require 'the_grid/api/command'
|
4
|
+
require 'the_grid/builder'
|
5
|
+
require 'the_grid/config'
|
6
|
+
Dir.chdir(File.dirname(__FILE__)) do
|
7
|
+
Dir['the_grid/builder/**/*.rb', 'the_grid/api/command/**/*.rb'].each{ |f| require f }
|
8
|
+
end
|
9
|
+
|
10
|
+
module TheGrid
|
11
|
+
def self.configure
|
12
|
+
Config.new.tap{ |c| yield c }.apply
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.build_for(relation)
|
16
|
+
Api.new(relation)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TheGrid::Api::Command::BatchRemove do
|
4
|
+
let(:table) { double(:primary_key => double.as_null_object) }
|
5
|
+
let(:relation) { double(:table => table).tap{ |r| r.stub(:scoped => r, :where => r) } }
|
6
|
+
|
7
|
+
it "raise exception when item_ids is blank" do
|
8
|
+
expect{
|
9
|
+
subject.execute_on(relation, :item_ids => [])
|
10
|
+
}.to raise_error ArgumentError
|
11
|
+
end
|
12
|
+
|
13
|
+
it "is a batch command" do
|
14
|
+
subject.batch?.should be_true
|
15
|
+
end
|
16
|
+
|
17
|
+
context "when item_ids is present" do
|
18
|
+
let(:item_ids) { [1, 2, 'non-int', 3, 4, '5'] }
|
19
|
+
let(:int_ids) { item_ids.reject{ |id| id.to_i <= 0 } }
|
20
|
+
|
21
|
+
before(:each) { relation.stub(:destroy_all => int_ids) }
|
22
|
+
after(:each) { subject.execute_on(relation, :item_ids => item_ids) }
|
23
|
+
|
24
|
+
it "remove items based on by primary key" do
|
25
|
+
relation.table.primary_key.should_receive(:in)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "reject non-integer ids" do
|
29
|
+
relation.table.primary_key.should_receive(:in).with(int_ids)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "remove only records with specified ids" do
|
33
|
+
relation.should_receive(:where).with(table.primary_key.in(int_ids))
|
34
|
+
end
|
35
|
+
|
36
|
+
it "destroy records" do
|
37
|
+
relation.should_receive(:destroy_all)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TheGrid::Api::Command::BatchUpdate do
|
4
|
+
let(:table) { double(:primary_key => double(:name => 'id').as_null_object) }
|
5
|
+
let(:relation) { double(:table => table).tap{ |r| r.stub(:scoped => r, :where => r) } }
|
6
|
+
|
7
|
+
it "raise exception when items is blank" do
|
8
|
+
expect{
|
9
|
+
subject.execute_on(relation, :items => [])
|
10
|
+
}.to raise_error ArgumentError
|
11
|
+
end
|
12
|
+
|
13
|
+
it "is a batch command" do
|
14
|
+
subject.batch?.should be_true
|
15
|
+
end
|
16
|
+
|
17
|
+
context "when items is present" do
|
18
|
+
let(:non_valid_items) { 2.times.map{ |i| {'id' => "string_#{i}", 'name' => "test_#{i}"} } }
|
19
|
+
let(:valid_items) { 4.times.map{ |i| {'id' => i +1 , 'name' => "test_#{i}"} } }
|
20
|
+
let(:valid_ids) { valid_items.map{ |r| r['id'] } }
|
21
|
+
let(:records) { valid_items.map{ |r| double(r.merge :update_attributes => true) } }
|
22
|
+
|
23
|
+
before(:each) { relation.stub(:where => records) }
|
24
|
+
after(:each) { subject.execute_on(relation, :items => valid_items + non_valid_items) }
|
25
|
+
|
26
|
+
it "remove items based on by primary key" do
|
27
|
+
relation.table.primary_key.should_receive(:in)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "reject items with non-integer ids" do
|
31
|
+
relation.table.primary_key.should_receive(:in).with(valid_ids)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "remove only records with specified ids" do
|
35
|
+
relation.should_receive(:where).with(table.primary_key.in(valid_ids))
|
36
|
+
end
|
37
|
+
|
38
|
+
it "update attributes with given data" do
|
39
|
+
rows = records.index_by(&:id)
|
40
|
+
valid_items.each{ |data| rows.fetch(data['id']).should_receive(:update_attributes).with(data.except('id')) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
shared_examples "for a range filter" do
|
4
|
+
let(:filters){ { :field => filter } }
|
5
|
+
|
6
|
+
context "when both boundaries are specified" do
|
7
|
+
it "adds 'from' filter" do
|
8
|
+
relation.table[:field].should_receive(:gteq).with(value[:from])
|
9
|
+
end
|
10
|
+
|
11
|
+
it "adds 'to' filter" do
|
12
|
+
relation.table[:field].should_receive(:lteq).with(value[:to])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context "when left boundary is missed" do
|
17
|
+
before(:each){ filters[:field] = filter.except(:from) }
|
18
|
+
|
19
|
+
it "adds 'to' filter" do
|
20
|
+
relation.table[:field].should_receive(:lteq).with(value[:to])
|
21
|
+
end
|
22
|
+
|
23
|
+
it "does not add 'from' filter" do
|
24
|
+
relation.table[:field].should_not_receive(:gteq)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "when right boundary is missed" do
|
29
|
+
before(:each){ filters[:field] = filter.except(:to) }
|
30
|
+
|
31
|
+
it "adds 'from' filter " do
|
32
|
+
relation.table[:field].should_receive(:gteq).with(value[:from])
|
33
|
+
end
|
34
|
+
|
35
|
+
it "does not add 'to' filter" do
|
36
|
+
relation.table[:field].should_not_receive(:lteq)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe TheGrid::Api::Command::Filter do
|
42
|
+
let(:table) { double(:blank? => false).as_null_object }
|
43
|
+
let(:relation) { double(:table => table).tap{ |r| r.stub(:where => r) } }
|
44
|
+
|
45
|
+
after(:each) { subject.execute_on(relation, :filters => filters) }
|
46
|
+
|
47
|
+
context "when filters are missed" do
|
48
|
+
let(:filters){ Hash.new }
|
49
|
+
|
50
|
+
it "returns the same relation object" do
|
51
|
+
relation.should_not_receive(:where)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context "when filters with primary values" do
|
56
|
+
let(:filters){ { :id => [1, 2], :state => "test" } }
|
57
|
+
|
58
|
+
it "changes relation conditions" do
|
59
|
+
relation.should_receive(:where)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "filters by array of values" do
|
63
|
+
relation.table[:id].should_receive(:in).with(filters[:id])
|
64
|
+
end
|
65
|
+
|
66
|
+
it "filters by value" do
|
67
|
+
relation.table[:state].should_receive(:eq).with(filters[:state])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "when filters with date range" do
|
72
|
+
let(:filter) { {:from => '2013-02-12 12:20:21', :to => '2013-05-12 13:20:01', :type => :date } }
|
73
|
+
let(:value) { Hash[filter.except(:type).map{ |k, v| [k, v.to_time] }] }
|
74
|
+
include_examples "for a range filter"
|
75
|
+
end
|
76
|
+
|
77
|
+
context "when filters with time range" do
|
78
|
+
let(:filter) { {:from => 2.days.ago.to_f, :to => Time.now.to_f, :type => :time } }
|
79
|
+
let(:value) { Hash[filter.except(:type).map{ |k, v| [k, Time.at(v)] }] }
|
80
|
+
include_examples "for a range filter"
|
81
|
+
end
|
82
|
+
|
83
|
+
context "when filters with non-date range" do
|
84
|
+
let(:filter) { {:from => 10, :to => 20} }
|
85
|
+
let(:value) { filter }
|
86
|
+
include_examples "for a range filter"
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TheGrid::Api::Command::Paginate do
|
4
|
+
let(:relation){ double("ActiveRecord::Relation", :count => 25).as_null_object }
|
5
|
+
|
6
|
+
context "when options are missed" do
|
7
|
+
after(:each){ subject.execute_on(relation, {}) }
|
8
|
+
|
9
|
+
it "returns first page" do
|
10
|
+
relation.should_receive(:offset).with(0)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "returns default amount of items" do
|
14
|
+
relation.should_receive(:limit).with(subject.class.default_per_page)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context "when options are specified" do
|
19
|
+
after(:each){ subject.execute_on(relation, :page => 3, :per_page => 5) }
|
20
|
+
|
21
|
+
it "returns specified page" do
|
22
|
+
relation.should_receive(:offset).with(10)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "returns specified amount of items" do
|
26
|
+
relation.should_receive(:limit).with(5)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "when calculates max page" do
|
31
|
+
it "should use default per_page option" do
|
32
|
+
subject.calculate_max_page_for(relation, {}).should eql (25.0 / subject.class.default_per_page).ceil
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should respect specified per_page option" do
|
36
|
+
subject.calculate_max_page_for(relation, :per_page => 15).should eql 2
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it "calculates max page when prepares context" do
|
41
|
+
subject.contextualize(relation, :page => 1, :per_page => 15).fetch(:max_page).should eql 2
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TheGrid::Api::Command::Search do
|
4
|
+
let(:columns) {{ :name => string_column, :status => string_column, :id => double(:type => :int) }}
|
5
|
+
let(:relation) { double(:columns_hash => columns, :column_names => columns.keys, :table => columns).as_null_object }
|
6
|
+
|
7
|
+
after(:each) { subject.execute_on(relation, options) }
|
8
|
+
|
9
|
+
context "when query or search_over is missed" do
|
10
|
+
let(:options) { Hash.new }
|
11
|
+
|
12
|
+
it "does not search if query is missed" do
|
13
|
+
relation.should_not_receive(:where)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "does not search over nested relations if search_over is missed" do
|
17
|
+
relation.should_not_receive(:reflections)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context "when searchable_columns is missed" do
|
22
|
+
let(:options) {{ :query => "test" }}
|
23
|
+
|
24
|
+
it "search over own columns" do
|
25
|
+
relation.should_receive(:columns_hash)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "does not search over non-string columns" do
|
29
|
+
relation.table[:id].should_not_receive(:matches)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "combine searchable conditions by or" do
|
33
|
+
relation.table[:name].should_receive(:or).with(relation.table[:status])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when options are valid" do
|
38
|
+
let(:options) {{ :query => "test", :searchable_columns => [:name] }}
|
39
|
+
|
40
|
+
it "search by query" do
|
41
|
+
relation.should_receive(:where)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "search only by specified columns" do
|
45
|
+
relation.table[:status].should_not_receive(:matches)
|
46
|
+
relation.table[:id].should_not_receive(:matches)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "search using SQL LIKE" do
|
50
|
+
relation.table[:name].should_receive(:matches).with("%#{options[:query]}%")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "when search over nested relations" do
|
55
|
+
pending "logic is too complicated; maybe will be rewritten"
|
56
|
+
end
|
57
|
+
|
58
|
+
def string_column
|
59
|
+
double(:type => :string).tap do |c|
|
60
|
+
c.stub(:matches => c, :or => c)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TheGrid::Api::Command::Sort do
|
4
|
+
let(:table) { double(:name => "table_for_sort", :present? => true).as_null_object }
|
5
|
+
let(:relation) { double(:table_name => table.name, :table => table) }
|
6
|
+
let(:options) { {:field => "name", :order => "desc" } }
|
7
|
+
|
8
|
+
after(:each){ subject.execute_on(relation, options) }
|
9
|
+
|
10
|
+
it "sort by asc if order invalid" do
|
11
|
+
options[:order] = 'wrong order'
|
12
|
+
relation.should_receive(:order).with("#{table.name}.#{options[:field]} asc")
|
13
|
+
end
|
14
|
+
|
15
|
+
it "sort by specified order" do
|
16
|
+
relation.should_receive(:order).with("#{table.name}.#{options[:field]} #{options[:order]}")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "does not prepend field with table name if field is an alias" do
|
20
|
+
table.stub(:present? => false)
|
21
|
+
relation.should_receive(:order).with("#{options[:field]} #{options[:order]}")
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module GridCommands
|
4
|
+
class Sort < TheGrid::Api::Command::Sort; end
|
5
|
+
end
|
6
|
+
|
7
|
+
describe TheGrid::Api::Command do
|
8
|
+
subject{ TheGrid::Api::Command }
|
9
|
+
let(:commands_scope) { GridCommands }
|
10
|
+
|
11
|
+
it "can be executed on relation" do
|
12
|
+
subject.find(:paginate).should respond_to(:execute_on)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "build command instance" do
|
16
|
+
subject.find(:paginate).should be_kind_of subject.const_get('Paginate')
|
17
|
+
end
|
18
|
+
|
19
|
+
it "build flyweight instances" do
|
20
|
+
subject.find(:paginate).object_id.should eql subject.find(:paginate).object_id
|
21
|
+
end
|
22
|
+
|
23
|
+
it "raise error if command not found" do
|
24
|
+
expect{ subject.find(:unknown_cmd) }.to raise_error ArgumentError
|
25
|
+
end
|
26
|
+
|
27
|
+
it "has only one scope for commands by default" do
|
28
|
+
subject.scopes.should eql [ subject.to_s.underscore ]
|
29
|
+
end
|
30
|
+
|
31
|
+
context "when register new scope" do
|
32
|
+
before(:each) { subject.register_lookup_scope commands_scope.to_s.underscore }
|
33
|
+
after(:each) { subject.scopes.shift() }
|
34
|
+
|
35
|
+
it "put scope at the top" do
|
36
|
+
subject.scopes.first.should eql commands_scope.to_s.underscore
|
37
|
+
end
|
38
|
+
|
39
|
+
it "build the first found command" do
|
40
|
+
subject.find(:sort).should be_kind_of commands_scope.const_get('Sort')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|