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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1a1fa5246fe01b026cb2cb2eea4d8a1bdf1bb864
4
+ data.tar.gz: d41e7ccd372d8f2a63b702c77521a874e4baf4bc
5
+ SHA512:
6
+ metadata.gz: ab12d327506c9abfc4083855019fadffcddd399db62098b44f788a5abc91e823969df57e140214f9c5d38fb6047de374dc920281c128ae84c649d7c51235ac42
7
+ data.tar.gz: 8a0c9608f4dfb7deb525674287114fef5f6090bca34ec10daa1a0e8b8df4342c0cb7ae6b99382ff5053323774d63d2fa64b4c30cdfa16357b14a45a22bcb9563
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015 Yevhen Shemet
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # Tablets
2
+
3
+ ## Installation
4
+
5
+ Add this line to your application's `Gemfile`:
6
+
7
+ ```ruby
8
+ gem 'tablets'
9
+ ```
10
+
11
+ Include tablets and jquery datatables scripts in your `app/assets/application.js`:
12
+ ```
13
+ //= require dataTables/jquery.dataTables
14
+ //= require tablets
15
+ ```
16
+
17
+ Include jquery datatables styles in your `app/assets/application.css`:
18
+ ```
19
+ *= require dataTables/jquery.dataTables
20
+ ```
21
+
22
+ Mount engine in `config/routes.rb`:
23
+ ```
24
+ mount Tablets::Engine, at: '/tablets'
25
+ ```
26
+
27
+ ## Examples
28
+
29
+ In `app/tablets/posts.rb`:
30
+ ```ruby
31
+ Tablets.register :posts do
32
+ # Authorization logic.
33
+ authorize do |controller|
34
+ controller.current_user.id == params[:author_id]
35
+ end
36
+
37
+ # jQuery-datatables options overrides.
38
+ options do
39
+ { order: [0, 'asc'] }
40
+ end
41
+
42
+ # Columns definitions.
43
+ columns do
44
+ [{
45
+ title: 'Title',
46
+ order: 'posts.title',
47
+ data: :title,
48
+ options: { className: 'post-title' }
49
+ }, {
50
+ title: 'User',
51
+ order: ['users.first_name', 'users.last_name'],
52
+ data: ->(user) { [user.first_name, user.last_name].compact.join(' ') },
53
+ search: ['users.first_name', 'users.last_name']
54
+ }]
55
+ end
56
+
57
+ # HTML for details area.
58
+ details do |post|
59
+ post.content.html_safe
60
+ end
61
+
62
+ # Process records before extracting data.
63
+ process do |posts|
64
+ posts.map { |post| post.use_wrapper }
65
+ end
66
+
67
+ # Data relation.
68
+ relation do |params|
69
+ # Note: User joined to enable ordering.
70
+ User.joins(:users).where(user_id: params[:user_id])
71
+ end
72
+ end
73
+ ```
74
+
75
+ In `app/views/posts/index.html.erb`
76
+ ```erb
77
+ <%= render_tablet :posts, user_id: current_user.id %>
78
+ ```
79
+
80
+ ## JavaScripts callbacks
81
+
82
+ Called just before datatable initialization.
83
+ ```javascript
84
+ Tablets.Callbacks.beforeInit = function(element, options) {}
85
+ ```
86
+
87
+ Called after datatable initialization.
88
+ ```javascript
89
+ Tablets.Callbacks.afterInit = function(table) {}
90
+ ```
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Tablets'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('lib/**/*.rb')
14
+ end
15
+
16
+ Bundler::GemHelper.install_tasks
17
+
18
+ require 'rspec/core'
19
+ require 'rspec/core/rake_task'
@@ -0,0 +1,3 @@
1
+ Tablets.Callbacks =
2
+ beforeInit: (element, options) ->
3
+ afterInit: (table) ->
@@ -0,0 +1,35 @@
1
+ class Tablets.Tablet
2
+ constructor: (@element, @options, @params) ->
3
+ @initTable()
4
+ @initDetails()
5
+
6
+ dataTableOptions: ->
7
+ $.extend {}, @options,
8
+ fnServerParams: (data) =>
9
+ data.params = @params
10
+
11
+ initTable: ->
12
+ options = @dataTableOptions()
13
+
14
+ Tablets.Callbacks.beforeInit(@element, options)
15
+
16
+ @table = $(@element).DataTable(options)
17
+
18
+ Tablets.Callbacks.afterInit(@table)
19
+
20
+ initDetails: ->
21
+ table = @table
22
+ table.on 'click', 'tbody > tr[role=row] > td:first-child', ->
23
+ tr = $(this).closest 'tr'
24
+ row = table.row tr
25
+
26
+ if row.child.isShown()
27
+ row.child.hide()
28
+ tr.removeClass 'expanded'
29
+ tr.removeClass 'shown'
30
+ else
31
+ data = row.data().details
32
+ if data
33
+ row.child("<span>#{data}</span>").show()
34
+ tr.addClass 'expanded'
35
+ tr.addClass 'shown'
@@ -0,0 +1,4 @@
1
+ #= require_self
2
+ #= require_tree './tablets'
3
+
4
+ @Tablets = {}
@@ -0,0 +1,31 @@
1
+ module Tablets
2
+ # Responsible to provide data to tablet on ajax request.
3
+ class AjaxController < ApplicationController
4
+ before_filter :authorize!
5
+
6
+ def data
7
+ render json: Tablets::Data.new(tablet, data_params)
8
+ end
9
+
10
+ private
11
+
12
+ def tablet
13
+ @tablet ||= Tablets[params[:name].to_sym]
14
+ end
15
+
16
+ def authorize!
17
+ render nothing: true, status: 401 unless authorized?
18
+ end
19
+
20
+ def authorized?
21
+ tablet.authorize(self)
22
+ rescue
23
+ false
24
+ end
25
+
26
+ def data_params
27
+ params.slice(:name, :start, :length, :search,
28
+ :order, :params, :draw, :columns)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ <table class="table tablet" id="<%= id %>" data-name="<%= name %>">
2
+ <thead>
3
+ <tr>
4
+ <% columns.each.with_index do |column, index| %>
5
+ <th class="<%= 'nosort' unless column[:order].present? %>">
6
+ <%= column[:title] %>
7
+ </th>
8
+ <% end %>
9
+ </tr>
10
+ </thead>
11
+ <tbody>
12
+ </tbody>
13
+ </table>
14
+
15
+ <script>
16
+ new Tablets.Tablet(
17
+ $('#<%= id %>'),
18
+ <%= raw options.to_json %>,
19
+ <%= raw params.to_json %>
20
+ );
21
+ </script>
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ Tablets::Engine.routes.draw do
2
+ get 'ajax/:name/data', to: 'tablets/ajax#data', as: :data
3
+ end
@@ -0,0 +1,23 @@
1
+ module Tablets
2
+ class Data
3
+ module Processing
4
+ # Base class for relation processings.
5
+ class Base
6
+ # Initializes processor with relation and data reqired for processing.
7
+ def initialize(params, columns)
8
+ @params = params
9
+ @columns = columns
10
+ end
11
+
12
+ # Applies processing on relation. Need to be implemented in descendants.
13
+ def apply(relation)
14
+ fail NotImplementedError, '#apply need to be overrided by processing.'
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :params, :columns
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,47 @@
1
+ require 'tablets/utils/arel'
2
+
3
+ require 'tablets/data/processing/base'
4
+
5
+ module Tablets
6
+ class Data
7
+ module Processing
8
+ # Incapsulate relation filtering logic.
9
+ class Filter < Tablets::Data::Processing::Base
10
+ # Applies filter processing on relation.
11
+ def apply(relation)
12
+ search(relation)
13
+ end
14
+
15
+ private
16
+
17
+ # Applies search conditions if need.
18
+ def search(relation)
19
+ return relation unless params[:search].try(:[], :value).present?
20
+
21
+ conditions = build_conditions_for(params[:search][:value])
22
+ relation = relation.where(conditions) if conditions
23
+ relation
24
+ end
25
+
26
+ # Builds search conditions.
27
+ def build_conditions_for(query)
28
+ query.split(' ').map do |value|
29
+ searchable_columns.map do |column|
30
+ search_condition(column, value)
31
+ end.reduce(:or)
32
+ end.reduce(:and)
33
+ end
34
+
35
+ # Returns searchable columns.
36
+ def searchable_columns
37
+ columns.map { |column| column[:search] }.flatten.compact
38
+ end
39
+
40
+ # Returs search condition.
41
+ def search_condition(column, value)
42
+ Tablets::Utils::Arel.column(column).matches("%#{value}%")
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,34 @@
1
+ require 'tablets/data/processing/base'
2
+
3
+ module Tablets
4
+ class Data
5
+ module Processing
6
+ # Incapsulate relation ordering logic.
7
+ class Order < Tablets::Data::Processing::Base
8
+ # Applies order processing on relation.
9
+ def apply(relation)
10
+ params[:order].values.inject(relation) do |relation, item|
11
+ relation.order("#{column(item)} #{direction(item)}")
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ # Determines order column from params.
18
+ def column(item)
19
+ columns[item[:column].to_i][:order]
20
+ end
21
+
22
+ # Determines order direction from params.
23
+ # ASC by default.
24
+ def direction(item)
25
+ if %w(ASC DESC).include?(item[:dir].upcase)
26
+ item[:dir].upcase
27
+ else
28
+ 'ASC'
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,40 @@
1
+ require 'tablets/data/processing/base'
2
+
3
+ module Tablets
4
+ class Data
5
+ module Processing
6
+ # Incapsulate relation pagination logic.
7
+ class Paginate < Tablets::Data::Processing::Base
8
+ # Applies :start and :length from params to relation
9
+ # as offset and limit.
10
+ # :length is optional. Default value is used if no length is provided.
11
+ # :start recalculated to match beginnning of page.
12
+ def apply(relation)
13
+ relation.offset(offset).limit(per_page)
14
+ end
15
+
16
+ private
17
+
18
+ # Calculates offset as beginning of page.
19
+ def offset
20
+ (page - 1) * per_page
21
+ end
22
+
23
+ # Calculates page from start.
24
+ def page
25
+ (params[:start].to_i / per_page) + 1
26
+ end
27
+
28
+ # Returns default length.
29
+ def default_length
30
+ 10
31
+ end
32
+
33
+ # Returns length or default value.
34
+ def per_page
35
+ params.fetch(:length, default_length).to_i
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,61 @@
1
+ require 'tablets/data/processing/filter'
2
+ require 'tablets/data/processing/paginate'
3
+ require 'tablets/data/processing/order'
4
+
5
+ module Tablets
6
+ class Data
7
+ # Incapsulates database query.
8
+ class Query
9
+ # Initializes query with relation, params and columns.
10
+ def initialize(relation, params, columns)
11
+ @relation = relation
12
+ @params = params
13
+ @columns = columns
14
+ end
15
+
16
+ # Applies all processings on relation and returns it.
17
+ def fetch
18
+ result = relation
19
+ result = order(result)
20
+ result = filter(result)
21
+ result = paginate(result)
22
+ result
23
+ end
24
+
25
+ # Returns total records count before filter and pagination is applied.
26
+ def total
27
+ relation.count(:all)
28
+ end
29
+
30
+ # Returns records count after filter is applied but before pagination.
31
+ def filtered
32
+ filter(relation).count(:all)
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :relation, :params, :columns
38
+
39
+ # Applies order processing.
40
+ def order(records)
41
+ return records unless params[:order].present?
42
+
43
+ Tablets::Data::Processing::Order.new(params, columns).apply(records)
44
+ end
45
+
46
+ # Applies filter processing.
47
+ def filter(records)
48
+ return records unless params[:search].present?
49
+
50
+ Tablets::Data::Processing::Filter.new(params, columns).apply(records)
51
+ end
52
+
53
+ # Applies paginate processing.
54
+ def paginate(records)
55
+ return records if params.try(:[], :length) == '-1'
56
+
57
+ Tablets::Data::Processing::Paginate.new(params, columns).apply(records)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,58 @@
1
+ require 'tablets/data/query'
2
+
3
+ module Tablets
4
+ # Responsible to fetch and prepare data for jquery-datatables.
5
+ class Data
6
+ # Initializes data with tablet and params.
7
+ def initialize(tablet, params)
8
+ @tablet = tablet
9
+ @params = params
10
+ end
11
+
12
+ # Prepares data to render as json.
13
+ def as_json(_options = {})
14
+ {
15
+ draw: params[:draw].to_i,
16
+ recordsTotal: query.total,
17
+ recordsFiltered: query.filtered,
18
+ data: data
19
+ }
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :params, :tablet
25
+
26
+ # Initializes query with concretized relation.
27
+ def query
28
+ @query ||= Tablets::Data::Query.new(tablet.relation(params[:params]),
29
+ params,
30
+ tablet.columns)
31
+ end
32
+
33
+ # Fetching records and applies process tablet callback on it.
34
+ def records
35
+ @records ||= tablet.process(query.fetch)
36
+ end
37
+
38
+ # Fetching columns data for each row using column[:data] value.
39
+ # If column[:data] is symbol send it to the record.
40
+ # If column[:data] is proc calls it on the record.
41
+ # Also appends details.
42
+ def data
43
+ records.map do |record|
44
+ tablet.columns.map.with_index do |column, index|
45
+ [index, cell(record, column)]
46
+ end.to_h.merge(details: tablet.details(record))
47
+ end
48
+ end
49
+
50
+ # Returns single cell value, for specified record and for specified column.
51
+ def cell(record, column)
52
+ case column[:data]
53
+ when Symbol then record.send(column[:data])
54
+ when Proc then column[:data].call(record)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,9 @@
1
+ require 'rails/engine'
2
+
3
+ module Tablets
4
+ # Rails mountable engine.
5
+ class Engine < ::Rails::Engine
6
+ require 'jquery-datatables-rails'
7
+ engine_name 'tablets'
8
+ end
9
+ end
@@ -0,0 +1,49 @@
1
+ require 'rails'
2
+
3
+ module Tablets
4
+ module Global
5
+ # Responsible to make application configurable.
6
+ module Configurator
7
+ attr_accessor :options
8
+ attr_accessor :tablets_dir
9
+
10
+ # Setup configuration. Used in initializers.
11
+ def setup
12
+ yield self
13
+ end
14
+
15
+ # Initialize default config values.
16
+ def self.extended(base)
17
+ base.options = DEFAULT_OPTIONS
18
+ base.tablets_dir = File.expand_path('app/tablets', Rails.root)
19
+ end
20
+
21
+ DEFAULT_OPTIONS = {
22
+ pageLength: 15,
23
+ processing: false,
24
+ info: true,
25
+ lengthChange: false,
26
+ serverSide: true,
27
+ pagingType: 'full_numbers',
28
+ order: [0, 'asc'],
29
+ autoWidth: true,
30
+ orderCellsTop: true,
31
+ language: {
32
+ lengthMenu: '<span class=\'seperator\'>|</span>View _MENU_ records',
33
+ info: '<span class=\'seperator\'>|</span>Found total _TOTAL_ records',
34
+ infoEmpty: '',
35
+ emptyTable: 'No records found to show',
36
+ zeroRecords: 'No matching records found',
37
+ paginate: {
38
+ previous: 'Prev',
39
+ next: 'Next',
40
+ last: 'Last',
41
+ first: 'First',
42
+ page: 'Page',
43
+ pageOf: 'of'
44
+ }
45
+ }
46
+ }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,37 @@
1
+ module Tablets
2
+ module Global
3
+ # Tablets loader. Manages files loading from app tablets directory.
4
+ module Loader
5
+ # Unloads tablet.
6
+ def unload!
7
+ @tablets = nil
8
+ end
9
+
10
+ # Checks if tablet is loading.
11
+ # Tablets is not loaded only if tablets is nil.
12
+ def loaded?
13
+ !@tablets.nil?
14
+ end
15
+
16
+ # Loads tablets files.
17
+ def load!
18
+ @tablets = {}
19
+ files.each { |file| load file }
20
+ end
21
+
22
+ private
23
+
24
+ # Load paths.
25
+ def load_paths
26
+ [Tablets.tablets_dir]
27
+ end
28
+
29
+ # Prepares files list using load paths.
30
+ def files
31
+ load_paths.flatten.compact.uniq.flat_map do |path|
32
+ Dir["#{path}/**/*.rb"]
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ require 'tablets/tablet'
2
+
3
+ module Tablets
4
+ module Global
5
+ # Tables store.
6
+ # Expects @tablets to be defined elsewhere.
7
+ module Store
8
+ extend Forwardable
9
+
10
+ attr_reader :tablets
11
+
12
+ def_delegators :tablets, :[]
13
+
14
+ # Initializes tablet and put tablet into store.
15
+ #
16
+ # Tablets.register(:user) do
17
+ # # Tablet configuration.
18
+ # end
19
+ #
20
+ def register(name, &block)
21
+ @tablets[name] = Tablets::Tablet.new(name, &block)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ require 'rails/railtie'
2
+
3
+ require 'tablets/view_helpers'
4
+
5
+ module Tablets
6
+ # Rails hooks.
7
+ class Railtie < ::Rails::Railtie
8
+ # Allows tablets view helper to be used anywhere in rails views.
9
+ initializer 'tablets.view_helpers' do
10
+ ActionView::Base.send :include, Tablets::ViewHelpers
11
+ end
12
+
13
+ # Manages tablets loading and cleanup.
14
+ # Also ensures tablets reloading in development environment.
15
+ initializer 'tablets.reloader' do |app|
16
+ if app.config.reload_classes_only_on_change
17
+ ActionDispatch::Reloader.to_prepare(prepend: true) { Tablets.unload! }
18
+ else
19
+ ActionDispatch::Reloader.to_cleanup { Tablets.unload! }
20
+ end
21
+
22
+ ActionDispatch::Reloader.to_prepare do
23
+ Tablets.load! unless Tablets.loaded?
24
+ end
25
+ end
26
+ end
27
+ end