tablets 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +90 -0
  4. data/Rakefile +19 -0
  5. data/app/assets/javascripts/tablets/callbacks.js.coffee +3 -0
  6. data/app/assets/javascripts/tablets/tablet.js.coffee +35 -0
  7. data/app/assets/javascripts/tablets.js.coffee +4 -0
  8. data/app/controllers/tablets/ajax_controller.rb +31 -0
  9. data/app/views/tablets/_tablet.html.erb +21 -0
  10. data/config/routes.rb +3 -0
  11. data/lib/tablets/data/processing/base.rb +23 -0
  12. data/lib/tablets/data/processing/filter.rb +47 -0
  13. data/lib/tablets/data/processing/order.rb +34 -0
  14. data/lib/tablets/data/processing/paginate.rb +40 -0
  15. data/lib/tablets/data/query.rb +61 -0
  16. data/lib/tablets/data.rb +58 -0
  17. data/lib/tablets/engine.rb +9 -0
  18. data/lib/tablets/global/configurator.rb +49 -0
  19. data/lib/tablets/global/loader.rb +37 -0
  20. data/lib/tablets/global/store.rb +25 -0
  21. data/lib/tablets/railtie.rb +27 -0
  22. data/lib/tablets/renderer.rb +84 -0
  23. data/lib/tablets/tablet.rb +60 -0
  24. data/lib/tablets/utils/arel.rb +34 -0
  25. data/lib/tablets/utils/config.rb +75 -0
  26. data/lib/tablets/version.rb +3 -0
  27. data/lib/tablets/view_helpers.rb +14 -0
  28. data/lib/tablets.rb +19 -0
  29. data/spec/lib/tablets/data/processing/base_spec.rb +19 -0
  30. data/spec/lib/tablets/data/processing/paginate_spec.rb +73 -0
  31. data/spec/lib/tablets/global/store_spec.rb +33 -0
  32. data/spec/lib/tablets/renderer_spec.rb +102 -0
  33. data/spec/lib/tablets/utils/config_spec.rb +104 -0
  34. data/spec/lib/tablets/view_helpers_spec.rb +42 -0
  35. data/spec/spec_helper.rb +92 -0
  36. metadata +169 -0
@@ -0,0 +1,84 @@
1
+ require 'tablets/engine'
2
+
3
+ module Tablets
4
+ # Prepares markup. Renders table without any data.
5
+ class Renderer
6
+ # Initializes renderer with tablet and params.
7
+ def initialize(tablet, params = {})
8
+ @tablet = tablet
9
+ @params = params
10
+ end
11
+
12
+ # Renders table in view_context.
13
+ def render(view_context)
14
+ view_context.render(partial: 'tablets/tablet', locals: locals)
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :params, :tablet
20
+
21
+ # Prepares locals for tablet partial.
22
+ def locals
23
+ {
24
+ id: id,
25
+ name: tablet.name,
26
+ columns: tablet.columns,
27
+ options: options,
28
+ params: params
29
+ }
30
+ end
31
+
32
+ # Id for HTML container. Includes name and params with values.
33
+ #
34
+ # renderer = Tablets::Renderer.new(:posts, user_id: 1)
35
+ # renderer.id #=> 'posts_user_id_1'
36
+ #
37
+ def id
38
+ [tablet.name, params.to_a].flatten.map(&:to_s).join('_')
39
+ end
40
+
41
+ # Calculates resulting options.
42
+ def options
43
+ @options ||= {}
44
+ .merge(Tablets.options)
45
+ .merge(columnDefs: columns_definitions, ajax: data_path)
46
+ .merge(tablet.options)
47
+ end
48
+
49
+ # Calculates column options and applies options on top.
50
+ #
51
+ # ...
52
+ # columns do
53
+ # [{
54
+ # title: 'Title',
55
+ # order: 'posts.title',
56
+ # data: :title,
57
+ # options: { className: 'post-title' }
58
+ # }]
59
+ # end
60
+ # ...
61
+ #
62
+ # renderer.columns_definitions #=> [{
63
+ # targets: 0,
64
+ # searchable: false,
65
+ # orderable: true,
66
+ # className: 'post-title'
67
+ # }]
68
+ #
69
+ def columns_definitions
70
+ tablet.columns.map.with_index do |column, index|
71
+ {
72
+ targets: index,
73
+ searchable: column[:search].present?,
74
+ orderable: column[:order].present?
75
+ }.merge!(column[:options] || {})
76
+ end
77
+ end
78
+
79
+ # Returns path to data.
80
+ def data_path
81
+ Tablets::Engine.routes.url_helpers.data_path(tablet.name)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,60 @@
1
+ require 'tablets/utils/config'
2
+
3
+ module Tablets
4
+ # Incapsulates tablet related information.
5
+ class Tablet
6
+ attr_reader :name
7
+
8
+ # Initializes tablet with name, callbacks fed with block.
9
+ def initialize(name, &block)
10
+ @name = name
11
+ @config = Tablets::Utils::Config.new(&block)
12
+ end
13
+
14
+ # Returns general jquery-datatable configuration overrides.
15
+ # By default returns empty options.
16
+ def options
17
+ call(:options) { {} }
18
+ end
19
+
20
+ # Determines is user authorized.
21
+ # By default returns true.
22
+ def authorize(controller)
23
+ call(:authorize, controller) { true }
24
+ end
25
+
26
+ # Allows to make additional processing before records would be used.
27
+ # By default returns records.
28
+ def process(records)
29
+ call(:process, records) { records }
30
+ end
31
+
32
+ # Returns details HTML for the record.
33
+ # By default returns nil.
34
+ def details(record)
35
+ call(:details, record) { nil }
36
+ end
37
+
38
+ # Returns columns definitions.
39
+ # Required.
40
+ def columns
41
+ call(:columns)
42
+ end
43
+
44
+ # Returns database relation to fetch data.
45
+ # Required.
46
+ def relation(params)
47
+ call(:relation, params)
48
+ end
49
+
50
+ private
51
+
52
+ # Calls callback.
53
+ # Clarifies error message on error.
54
+ def call(callback, *params, &block)
55
+ @config.call(callback, *params, &block)
56
+ rescue ArgumentError
57
+ raise ArgumentError, "Please define #{callback} for '#{name}' tablet."
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,34 @@
1
+ module Tablets
2
+ module Utils
3
+ # Arel related utils.
4
+ module Arel
5
+ class << self
6
+ # Casting arel column using db specific type.
7
+ def column(column)
8
+ model, column = column.split('.')
9
+ model = model.singularize.titleize.gsub(/ /, '').constantize
10
+
11
+ ::Arel::Nodes::NamedFunction.new(
12
+ 'CAST', [model.arel_table[column.to_sym].as(typecast)]
13
+ )
14
+ end
15
+
16
+ private
17
+
18
+ # Returns database specific string type.
19
+ def typecast
20
+ case db_adapter
21
+ when :postgresql then 'VARCHAR'
22
+ when :mysql2 then 'CHAR'
23
+ when :sqlite3 then 'TEXT'
24
+ end
25
+ end
26
+
27
+ # Retrieves rails database adapter.
28
+ def db_adapter
29
+ ActiveRecord::Base.configurations[Rails.env]['adapter'].to_sym
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,75 @@
1
+ module Tablets
2
+ module Utils
3
+ # Config utility. Allows to write configs in declarative form.
4
+ # And fetch values with defaults.
5
+ #
6
+ # config = Config.new do
7
+ # some_var 32
8
+ # another_var 42
9
+ #
10
+ # some_callback do |arg|
11
+ # do_something_with arg
12
+ # end
13
+ # end
14
+ #
15
+ # config.get(:some_var) #=> 32
16
+ # config.get(:non_existent_var) #=> ArgumentError
17
+ # config.get(:non_existent_var, 'default') #=> 'default'
18
+ #
19
+ # config.call(:some_callback, 'my_arg') { |arg| default_action arg }
20
+ #
21
+ class Config
22
+ # Initializes config with block.
23
+ # Block is optional and can be applied later.
24
+ def initialize(&block)
25
+ @hash = {}
26
+
27
+ apply(&block) unless block.nil?
28
+ end
29
+
30
+ # Executes block in config context.
31
+ def apply(&block)
32
+ instance_eval(&block)
33
+ end
34
+
35
+ # Returns value.
36
+ # If no value defined, returns default.
37
+ # If no default raises ArgumentError.
38
+ def get(name, default = nil, &default_block)
39
+ value = @hash[name][0] if @hash[name]
40
+
41
+ if !value.nil?
42
+ value
43
+ elsif !default.nil?
44
+ default
45
+ elsif !default_block.nil?
46
+ default_block.call
47
+ else
48
+ fail ArgumentError, "Value :#{name} is not set."
49
+ end
50
+ end
51
+
52
+ # Calls callback.
53
+ # If no calbback defined, calls default.
54
+ # If no default raises ArgumentError.
55
+ def call(name, *params, &default)
56
+ callback = @hash[name][0] if @hash[name]
57
+
58
+ if !callback.nil?
59
+ callback.call(*params)
60
+ elsif !default.nil?
61
+ default.call(*params)
62
+ else
63
+ fail ArgumentError, "Callback :#{name} is not registered."
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ # Gathers all calls.
70
+ def method_missing(name, *args, &block)
71
+ @hash[name] = [*args, block]
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,3 @@
1
+ module Tablets
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,14 @@
1
+ require 'tablets/renderer'
2
+
3
+ module Tablets
4
+ # View helpers included to Rails.
5
+ module ViewHelpers
6
+ # Finds tablet by name and renders is with params in current view context.
7
+ #
8
+ # <%= render_tablet :posts, user_id: @user.id %>
9
+ #
10
+ def render_tablet(name, params = {})
11
+ Tablets::Renderer.new(Tablets[name], params).render(self)
12
+ end
13
+ end
14
+ end
data/lib/tablets.rb ADDED
@@ -0,0 +1,19 @@
1
+ require 'tablets/global/loader'
2
+ require 'tablets/global/store'
3
+ require 'tablets/global/configurator'
4
+
5
+ # Top level tablets module. Extended with global tablets methods.
6
+ module Tablets
7
+ autoload :Data, 'tablets/data'
8
+
9
+ extend Tablets::Global::Loader
10
+ extend Tablets::Global::Store
11
+ extend Tablets::Global::Configurator
12
+ end
13
+
14
+ require 'tablets/version'
15
+
16
+ require 'tablets/tablet'
17
+
18
+ require 'tablets/railtie'
19
+ require 'tablets/engine'
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ require 'tablets/data/processing/base'
4
+
5
+ RSpec.describe Tablets::Data::Processing::Base do
6
+ subject { Tablets::Data::Processing::Base.new(params, columns) }
7
+
8
+ let(:relation) { 'relation' }
9
+ let(:params) { 'params' }
10
+ let(:columns) { 'columns' }
11
+
12
+ describe '#apply' do
13
+ it 'raises not implemented error' do
14
+ expect do
15
+ subject.apply(relation)
16
+ end.to raise_error(NotImplementedError)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ require 'tablets/data/processing/paginate'
4
+
5
+ RSpec.describe Tablets::Data::Processing::Paginate do
6
+ subject { Tablets::Data::Processing::Paginate.new(params, columns) }
7
+
8
+ let(:relation) { double }
9
+ let(:params) { { start: start, length: length } }
10
+ let(:columns) { 'columns' }
11
+
12
+ describe '#apply' do
13
+ before do
14
+ allow(relation).to receive(:offset).with(anything).and_return(relation)
15
+ allow(relation).to receive(:limit).with(anything).and_return(relation)
16
+ end
17
+
18
+ let!(:result) { subject.apply(relation) }
19
+
20
+ context 'if start matches beginning of page' do
21
+ let(:start) { 30 }
22
+ let(:length) { 15 }
23
+
24
+ it 'applies start' do
25
+ expect(relation).to have_received(:offset).with(start)
26
+ end
27
+
28
+ it 'applies length' do
29
+ expect(relation).to have_received(:limit).with(length)
30
+ end
31
+
32
+ it 'returns relation' do
33
+ expect(result).to eq(relation)
34
+ end
35
+ end
36
+
37
+ context 'if start not matches beginning of page' do
38
+ let(:start) { 25 }
39
+ let(:page_start) { 15 }
40
+ let(:length) { 15 }
41
+
42
+ it 'applies page start' do
43
+ expect(relation).to have_received(:offset).with(page_start)
44
+ end
45
+
46
+ it 'applies length' do
47
+ expect(relation).to have_received(:limit).with(length)
48
+ end
49
+
50
+ it 'returns relation' do
51
+ expect(result).to eq(relation)
52
+ end
53
+ end
54
+
55
+ context 'without length' do
56
+ let(:default_length) { 10 }
57
+ let(:start) { 10 }
58
+ let(:params) { { start: start } }
59
+
60
+ it 'applies offset' do
61
+ expect(relation).to have_received(:offset)
62
+ end
63
+
64
+ it 'applies default length' do
65
+ expect(relation).to have_received(:limit).with(default_length)
66
+ end
67
+
68
+ it 'returns relation' do
69
+ expect(result).to eq(relation)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ require 'tablets/global/store'
4
+
5
+ RSpec.describe Tablets::Global::Store do
6
+ subject { Object.new.tap { |object| object.extend(Tablets::Global::Store) } }
7
+
8
+ describe '#register' do
9
+ let(:name) { :posts }
10
+ let(:block) { proc {} }
11
+ let(:tablet) { double }
12
+
13
+ before do
14
+ subject.instance_eval { @tablets = {} }
15
+ allow(Tablets::Tablet).to receive(:new).with(name, &block)
16
+ .and_return(tablet)
17
+ end
18
+
19
+ let!(:result) { subject.register(name, &block) }
20
+
21
+ it 'initializes tablet' do
22
+ expect(Tablets::Tablet).to have_received(:new).with(name, &block)
23
+ end
24
+
25
+ it 'stores tablet' do
26
+ expect(subject.tablets).to eq(name => tablet)
27
+ end
28
+
29
+ it 'delegates [] to tablets' do
30
+ expect(subject[name]).to eq(subject.tablets[name])
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+
3
+ require 'tablets'
4
+ require 'tablets/renderer'
5
+
6
+ RSpec.describe Tablets::Renderer do
7
+ subject { Tablets::Renderer.new(tablet, params) }
8
+
9
+ describe '#render' do
10
+ let(:tablet) { double }
11
+ let(:params) { { user_id: 1 } }
12
+
13
+ let(:render_result) { 'result' }
14
+ let(:path) { 'path' }
15
+
16
+ let(:tablet_name) { :posts }
17
+ let(:tablet_columns) do
18
+ [{
19
+ title: 'Title',
20
+ order: 'posts.title',
21
+ data: :title,
22
+ options: { className: 'post-title' }
23
+ }, {
24
+ title: 'User',
25
+ order: ['users.first_name', 'users.last_name'],
26
+ data: :user,
27
+ search: ['users.first_name', 'users.last_name']
28
+ }]
29
+ end
30
+ let(:tablet_options) { { some_option: :some_value } }
31
+
32
+ let(:options) do
33
+ {
34
+ ajax: path,
35
+ columnDefs: [{
36
+ targets: 0,
37
+ searchable: false,
38
+ orderable: true,
39
+ className: 'post-title'
40
+ }, {
41
+ targets: 1,
42
+ searchable: true,
43
+ orderable: true
44
+ }]
45
+ }.merge!(tablet_options)
46
+ end
47
+
48
+ let(:view_context) { double }
49
+
50
+ before do
51
+ allow(tablet).to receive_messages(
52
+ name: tablet_name, columns: tablet_columns, options: tablet_options
53
+ )
54
+
55
+ allow(Tablets::Engine).to receive_message_chain(
56
+ :routes, :url_helpers, :data_path
57
+ ).and_return(path)
58
+
59
+ allow(view_context).to receive(:render).and_return(render_result)
60
+ end
61
+
62
+ let!(:result) { subject.render(view_context) }
63
+
64
+ def locals(local)
65
+ hash_including(locals: hash_including(local))
66
+ end
67
+
68
+ it 'renders tablets partial' do
69
+ expect(view_context).to have_received(:render).with(
70
+ hash_including(partial: 'tablets/tablet')
71
+ )
72
+ end
73
+
74
+ it 'passes correct id as partial locals' do
75
+ expect(view_context).to have_received(:render).with(
76
+ locals(id: 'posts_user_id_1')
77
+ )
78
+ end
79
+
80
+ it 'passes correct columns as partial locals' do
81
+ expect(view_context).to have_received(:render).with(
82
+ locals(columns: tablet_columns)
83
+ )
84
+ end
85
+
86
+ it 'passes correct config as partial locals' do
87
+ expect(view_context).to have_received(:render).with(
88
+ locals(options: hash_including(options))
89
+ )
90
+ end
91
+
92
+ it 'passes correct params as partial locals' do
93
+ expect(view_context).to have_received(:render).with(
94
+ locals(params: params)
95
+ )
96
+ end
97
+
98
+ it 'returns render result' do
99
+ expect(result).to eq(render_result)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,104 @@
1
+ require 'spec_helper'
2
+
3
+ require 'tablets/utils/config'
4
+
5
+ RSpec.describe Tablets::Utils::Config do
6
+ subject { Tablets::Utils::Config.new(&block) }
7
+
8
+ def hash
9
+ subject.instance_eval { @hash }
10
+ end
11
+
12
+ describe '#initialize' do
13
+ context 'with block' do
14
+ let(:block) { proc { var('val') } }
15
+
16
+ it 'applies block to config' do
17
+ expect(hash).to_not be_empty
18
+ end
19
+ end
20
+
21
+ context 'without block' do
22
+ let(:block) { nil }
23
+
24
+ it 'initializes empty config' do
25
+ expect(hash).to be_empty
26
+ end
27
+ end
28
+ end
29
+
30
+ describe '#apply' do
31
+ let(:block) { nil }
32
+ let(:another_block) { proc { var('val') } }
33
+
34
+ it 'applies block' do
35
+ subject.apply(&another_block)
36
+
37
+ expect(hash).to_not be_empty
38
+ end
39
+ end
40
+
41
+ describe '#get' do
42
+ context 'if value exists' do
43
+ let(:block) { proc { var('val') } }
44
+
45
+ it 'returns value' do
46
+ expect(subject.get(:var)).to eq('val')
47
+ end
48
+ end
49
+
50
+ context 'if value not exists' do
51
+ let(:block) {}
52
+
53
+ it 'raises error' do
54
+ expect do
55
+ subject.get(:var)
56
+ end.to raise_error(ArgumentError)
57
+ end
58
+ end
59
+
60
+ context 'if value not exists but have default' do
61
+ let(:block) {}
62
+
63
+ it 'returns default' do
64
+ expect(subject.get(:var, 'default')).to eq('default')
65
+ end
66
+ end
67
+
68
+ context 'if value not exists but have default block' do
69
+ let(:block) {}
70
+
71
+ it 'returns default block return value' do
72
+ expect(subject.get(:var) { 'default' }).to eq('default')
73
+ end
74
+ end
75
+ end
76
+
77
+ describe '#call' do
78
+ context 'if callback exists' do
79
+ let(:block) { proc { var { 'val' } } }
80
+
81
+ it 'returns callback return value' do
82
+ expect(subject.call(:var)).to eq('val')
83
+ end
84
+ end
85
+
86
+ context 'if callback not exists' do
87
+ let(:block) {}
88
+
89
+ it 'raises error' do
90
+ expect do
91
+ subject.call(:var)
92
+ end.to raise_error(ArgumentError)
93
+ end
94
+ end
95
+
96
+ context 'if value not exists but have default block' do
97
+ let(:block) {}
98
+
99
+ it 'returns default block return value' do
100
+ expect(subject.call(:var) { 'default' }).to eq('default')
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ require 'tablets'
4
+ require 'tablets/view_helpers'
5
+
6
+ RSpec.describe Tablets::ViewHelpers do
7
+ subject { Object.new.tap { |object| object.extend(Tablets::ViewHelpers) } }
8
+
9
+ describe '#render_tablet' do
10
+ let(:name) { :name }
11
+ let(:params) { 'params' }
12
+ let(:tablet) { double }
13
+ let(:renderer) { double }
14
+ let(:render_result) { 'render_result' }
15
+
16
+ before do
17
+ allow(Tablets).to receive(:[]).with(name).and_return(tablet)
18
+ allow(Tablets::Renderer).to receive(:new).with(tablet, params)
19
+ .and_return(renderer)
20
+ allow(renderer).to receive(:render).with(subject)
21
+ .and_return(render_result)
22
+ end
23
+
24
+ let!(:result) { subject.render_tablet(name, params) }
25
+
26
+ it 'finds tablet' do
27
+ expect(Tablets).to have_received(:[]).with(name)
28
+ end
29
+
30
+ it 'initializes renderer' do
31
+ expect(Tablets::Renderer).to have_received(:new).with(tablet, params)
32
+ end
33
+
34
+ it 'renders in current view context' do
35
+ expect(renderer).to have_received(:render).with(subject)
36
+ end
37
+
38
+ it 'returns render result' do
39
+ expect(result).to eq(render_result)
40
+ end
41
+ end
42
+ end