the_grid 1.0.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|