tablets 0.1.0

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.
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