gitter 1.1.3
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.
- data/.gitignore +5 -0
- data/.travis.yml +6 -0
- data/Gemfile +14 -0
- data/Guardfile +21 -0
- data/License +20 -0
- data/Rakefile +9 -0
- data/Readme.markdown +159 -0
- data/assets/images/sort_asc.gif +0 -0
- data/assets/images/sort_desc.gif +0 -0
- data/gitter.gemspec +28 -0
- data/lib/gitter.rb +13 -0
- data/lib/gitter/axis.rb +48 -0
- data/lib/gitter/base.rb +149 -0
- data/lib/gitter/breadcrumbs.rb +44 -0
- data/lib/gitter/cell.rb +11 -0
- data/lib/gitter/column.rb +142 -0
- data/lib/gitter/columns.rb +110 -0
- data/lib/gitter/controller.rb +19 -0
- data/lib/gitter/csv.rb +9 -0
- data/lib/gitter/driver.rb +26 -0
- data/lib/gitter/drivers/abstract_driver.rb +36 -0
- data/lib/gitter/drivers/active_record_driver.rb +84 -0
- data/lib/gitter/facet.rb +95 -0
- data/lib/gitter/filters.rb +4 -0
- data/lib/gitter/filters/abstract_filter.rb +94 -0
- data/lib/gitter/filters/block_filter.rb +16 -0
- data/lib/gitter/filters/column_filter.rb +50 -0
- data/lib/gitter/filters/select_filter.rb +43 -0
- data/lib/gitter/grid.rb +23 -0
- data/lib/gitter/header.rb +44 -0
- data/lib/gitter/helpers.rb +44 -0
- data/lib/gitter/i18n.rb +11 -0
- data/lib/gitter/model.rb +40 -0
- data/lib/gitter/pivot.rb +107 -0
- data/lib/gitter/pivot_grid.rb +23 -0
- data/lib/gitter/railtie.rb +8 -0
- data/lib/gitter/table.rb +149 -0
- data/lib/gitter/utils.rb +12 -0
- data/lib/gitter/version.rb +3 -0
- data/spec/breadcrumbs_spec.rb +24 -0
- data/spec/column_filter_spec.rb +80 -0
- data/spec/column_spec.rb +159 -0
- data/spec/facets_spec.rb +75 -0
- data/spec/grid_spec.rb +92 -0
- data/spec/helper_spec.rb +8 -0
- data/spec/i18n_spec.rb +39 -0
- data/spec/inputs_spec.rb +0 -0
- data/spec/locales/de.yml +10 -0
- data/spec/locales/en.yml +10 -0
- data/spec/range_filter_spec.rb +32 -0
- data/spec/scope_filter_spec.rb +12 -0
- data/spec/select_filter_spec.rb +22 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/support/database.rb +24 -0
- data/spec/support/person_grid.rb +71 -0
- metadata +152 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Gitter
|
4
|
+
module Breadcrumbs
|
5
|
+
include Utils
|
6
|
+
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
def breadcrumbs
|
10
|
+
@breadcrumbs ||= begin
|
11
|
+
p = {}
|
12
|
+
text = filters.map do |filter|
|
13
|
+
p[filter.label] = filter_value(filter.name)
|
14
|
+
end
|
15
|
+
p
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def render_breadcrumbs delim = '>', params = {}
|
20
|
+
delim_tag = h.content_tag :span, delim, {class: 'breadcrumb_delim'}
|
21
|
+
|
22
|
+
p = {}
|
23
|
+
breadcrumbs = filters.map do |filter|
|
24
|
+
value = filter_value filter.name
|
25
|
+
if value.present?
|
26
|
+
s = h.content_tag :span, "#{filter.label}:", class: 'breadcrumb_key'
|
27
|
+
s += h.content_tag :span, value, class: 'breadcrumb_value'
|
28
|
+
p[filter.name] = value
|
29
|
+
h.link_to s, url_for(scoped_params(p).merge(params))
|
30
|
+
else
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end.compact
|
34
|
+
|
35
|
+
if breadcrumbs.present?
|
36
|
+
h.content_tag :span, breadcrumbs.join(delim_tag), {class: 'breadcrumbs'}, false
|
37
|
+
else
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
data/lib/gitter/cell.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
module Gitter
|
2
|
+
|
3
|
+
class Column
|
4
|
+
|
5
|
+
attr_reader :grid, :name, :headers, :attr, :block, :order, :order_desc, :uniq, :map, :html_options
|
6
|
+
|
7
|
+
def initialize grid, name, opts = {}, &block
|
8
|
+
@grid, @name, @block = grid, name, block
|
9
|
+
@attr = opts.delete(:column){name}
|
10
|
+
@order = opts.delete :order
|
11
|
+
@order = attr if @order == true
|
12
|
+
@order_desc = opts.delete :order_desc
|
13
|
+
@uniq = opts.delete :uniq
|
14
|
+
@map = opts.delete(:map){true}
|
15
|
+
@html_options = opts.delete(:html_options){{}}
|
16
|
+
if opts.has_key?(:header) || opts.has_key?(:headers) # handle :header => false correctly
|
17
|
+
header_opts = opts.delete(:header){opts.delete(:headers)}
|
18
|
+
@headers = [header_opts].flatten.map do |header_opt|
|
19
|
+
case header_opt
|
20
|
+
when Hash
|
21
|
+
content = header_opt.delete(:content)
|
22
|
+
h_opts = header_opt
|
23
|
+
else
|
24
|
+
content = header_opt
|
25
|
+
h_opts = {}
|
26
|
+
end
|
27
|
+
Header.new grid, content, h_opts.merge(:column => self)
|
28
|
+
end
|
29
|
+
else
|
30
|
+
@headers = [Header.new(grid, name, opts.merge(:column => self))]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def cells model
|
35
|
+
res = if map && Array === model
|
36
|
+
model.map{|m| cells m}
|
37
|
+
else
|
38
|
+
cell model
|
39
|
+
end
|
40
|
+
|
41
|
+
# set consecutively equal cells to nil
|
42
|
+
if uniq && Array === res
|
43
|
+
current = nil
|
44
|
+
r = res.map do |el|
|
45
|
+
if el != current || current.nil?
|
46
|
+
current = el
|
47
|
+
else
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
r
|
52
|
+
else
|
53
|
+
res
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def ordered
|
58
|
+
d = grid.filtered_driver
|
59
|
+
|
60
|
+
return d unless ordered?
|
61
|
+
|
62
|
+
desc = case (p = grid.params[:desc])
|
63
|
+
when true, 'true' then order_desc || true
|
64
|
+
when false, 'false' then false
|
65
|
+
else p
|
66
|
+
end
|
67
|
+
|
68
|
+
case order
|
69
|
+
when :cell
|
70
|
+
array_sort(d,desc){|model|cell model}
|
71
|
+
when Proc
|
72
|
+
array_sort(d,desc){|model|grid.eval(order,model)}
|
73
|
+
else
|
74
|
+
d.order order, desc
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def desc?
|
79
|
+
@desc ||= to_boolean grid.params[:desc]
|
80
|
+
end
|
81
|
+
|
82
|
+
def ordered?
|
83
|
+
@ordered ||= grid.params[:order] == name.to_s
|
84
|
+
end
|
85
|
+
|
86
|
+
def link label = nil, params = {}, options = {}
|
87
|
+
label ||= headers.first.label
|
88
|
+
if @order
|
89
|
+
img = order_img_tag(options)
|
90
|
+
label = grid.h.content_tag :span, img + label if ordered?
|
91
|
+
grid.h.link_to label, order_params.deep_merge(params), options
|
92
|
+
else
|
93
|
+
label
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_s
|
98
|
+
"Column(#{name},ordered=#{ordered?},#{headers.size} headers)"
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def order_img_tag opts = {}
|
104
|
+
desc_img = opts.delete(:desc_img){'sort_desc.gif'}
|
105
|
+
asc_img = opts.delete(:asc_img){'sort_asc.gif'}
|
106
|
+
grid.h.image_tag( desc? ? desc_img : asc_img)
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_boolean s
|
110
|
+
not (s && s.match(/true|t|1$/i)).nil?
|
111
|
+
end
|
112
|
+
|
113
|
+
# if current params contain order for this column then revert direction
|
114
|
+
# else add order_params for this column to current params
|
115
|
+
def order_params
|
116
|
+
@order_params ||= begin
|
117
|
+
p = grid.params.dup
|
118
|
+
if ordered?
|
119
|
+
p[:desc] = !desc?
|
120
|
+
else
|
121
|
+
p = p.merge order: name, desc: false
|
122
|
+
end
|
123
|
+
grid.scoped_params p
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def cell model
|
128
|
+
grid.decorate model
|
129
|
+
if block
|
130
|
+
content = grid.eval block, model
|
131
|
+
else
|
132
|
+
model.send(attr) || ''
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def array_sort driver, desc
|
137
|
+
arr = driver.scope.map{|model| [yield(model),model]}
|
138
|
+
driver.new arr.sort{|a,b| (desc ? -1 : 1)*(a<=>b) }.map{|a|a[1]}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'gitter/column'
|
3
|
+
require 'gitter/header'
|
4
|
+
require 'gitter/cell'
|
5
|
+
|
6
|
+
module Gitter
|
7
|
+
module Columns
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
alias_method_chain :scope, :columns
|
12
|
+
end
|
13
|
+
|
14
|
+
def header_row
|
15
|
+
@current_header_row = []
|
16
|
+
yield
|
17
|
+
(@header_rows||=[]) << @current_header_row
|
18
|
+
end
|
19
|
+
|
20
|
+
def header *args
|
21
|
+
@current_header_row << Header.new(self,*args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def column name, opts = {}, &block
|
25
|
+
(@columns||= {})[name] = Column.new self, name, opts, &block
|
26
|
+
end
|
27
|
+
|
28
|
+
def scope_with_columns &scope
|
29
|
+
if scope
|
30
|
+
scope_without_columns &scope
|
31
|
+
else
|
32
|
+
@scope_with_columns ||= order_column ? order_column.ordered.scope : scope_without_columns
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def paginate *args
|
37
|
+
scope.paginate *args
|
38
|
+
end
|
39
|
+
|
40
|
+
def header_rows
|
41
|
+
@all_header_rows ||= begin
|
42
|
+
rows = @header_rows || []
|
43
|
+
max = columns.map{|col|col.headers.size}.max
|
44
|
+
columns_headers = columns.map{|col| Array.new(max){|i| col.headers[i] || Header.blank }}
|
45
|
+
rows += columns_headers.transpose
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def rows_for model
|
50
|
+
cols = columns.map{|c| [c.cells(model)].flatten }
|
51
|
+
max = cols.map{|col|col.size}.max
|
52
|
+
cols.map do |col|
|
53
|
+
nil_padded_cells = Array.new(max){|i| col[i]}
|
54
|
+
cells = []
|
55
|
+
nil_padded_cells.each_with_index do |c,i|
|
56
|
+
unless c.nil?
|
57
|
+
height = consecutive_count(nil_padded_cells.slice(i+1..-1), nil) + 1
|
58
|
+
cells << Cell.new(c, rowspan: height)
|
59
|
+
else # required for transpose
|
60
|
+
cells << nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
cells
|
64
|
+
end.transpose
|
65
|
+
end
|
66
|
+
|
67
|
+
def rows scope = nil
|
68
|
+
res = []
|
69
|
+
models(scope||self.scope).each{|model| res += rows_for(model)}
|
70
|
+
res
|
71
|
+
end
|
72
|
+
|
73
|
+
def models scope = self.scope
|
74
|
+
if respond_to? :transform
|
75
|
+
transform scope
|
76
|
+
else
|
77
|
+
scope
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def columns
|
82
|
+
(@columns||={}).values
|
83
|
+
end
|
84
|
+
|
85
|
+
def order_column
|
86
|
+
@order_column ||= begin
|
87
|
+
if order = @params[:order]
|
88
|
+
@columns[:"#{order}"] or raise ArgumentError, "invalid order column #{order}"
|
89
|
+
else
|
90
|
+
raise ArgumentError, ':desc given but no :order' if @params[:desc]
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def consecutive_count arr, what
|
97
|
+
count = 0
|
98
|
+
arr.each do |el|
|
99
|
+
if el == what
|
100
|
+
count +=1
|
101
|
+
else
|
102
|
+
break
|
103
|
+
end
|
104
|
+
end
|
105
|
+
count
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'artdeco'
|
2
|
+
|
3
|
+
module Gitter
|
4
|
+
module Controller
|
5
|
+
|
6
|
+
def self.included base
|
7
|
+
base.helper_method :render_grid, :decorate
|
8
|
+
end
|
9
|
+
|
10
|
+
def render_grid grid_class, opts = {}
|
11
|
+
grid_class.new self, opts
|
12
|
+
end
|
13
|
+
|
14
|
+
def decorate model, *decorator_classes
|
15
|
+
Artdeco::Decorator.new(self).decorate model, *decorator_classes
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/lib/gitter/csv.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Gitter
|
2
|
+
module Driver
|
3
|
+
|
4
|
+
def driver_class driver_class = nil
|
5
|
+
if driver_class
|
6
|
+
@driver_class = driver_class
|
7
|
+
else
|
8
|
+
@driver_class || detect_driver_class or raise ConfigurationError, "no driver given"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_driver scope
|
13
|
+
driver_class.new scope
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def detect_driver_class
|
18
|
+
case
|
19
|
+
when Module.const_defined?(:ActiveRecord)
|
20
|
+
require 'gitter/drivers/active_record_driver'
|
21
|
+
Gitter::ActiveRecordDriver
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Gitter
|
2
|
+
|
3
|
+
class AbstractDriver
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
attr_reader :scope
|
7
|
+
|
8
|
+
def initialize scope
|
9
|
+
@scope = scope
|
10
|
+
end
|
11
|
+
|
12
|
+
# methods to be implemented
|
13
|
+
|
14
|
+
# unordered
|
15
|
+
|
16
|
+
# order( attr, desc = nil)
|
17
|
+
|
18
|
+
# where( attr_values, opts = {} )
|
19
|
+
# where opts may be :exact, :ignore_case, :strip_blank, :find_format
|
20
|
+
|
21
|
+
# where_greater_or_equal( attr, value )
|
22
|
+
|
23
|
+
# where_less_or_equal( attr, value)
|
24
|
+
|
25
|
+
# each(&block)
|
26
|
+
|
27
|
+
# named_scope( name )
|
28
|
+
|
29
|
+
# distict_values( attr )
|
30
|
+
|
31
|
+
def new(scope)
|
32
|
+
self.class.new scope
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'gitter/drivers/abstract_driver'
|
2
|
+
module Gitter
|
3
|
+
|
4
|
+
class ActiveRecordDriver < AbstractDriver
|
5
|
+
|
6
|
+
delegate :count, to: :scope
|
7
|
+
|
8
|
+
def order attr, desc = nil
|
9
|
+
what = case desc
|
10
|
+
when true, 'true' then "#{attr} DESC"
|
11
|
+
when false, 'false' then attr
|
12
|
+
when String then desc
|
13
|
+
else attr
|
14
|
+
end
|
15
|
+
new scope.except(:order).order(what.to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
def unordered
|
19
|
+
new scope.except(:order)
|
20
|
+
end
|
21
|
+
|
22
|
+
def group arg
|
23
|
+
new scope.group(arg)
|
24
|
+
end
|
25
|
+
|
26
|
+
def where attr_values, opts = {}
|
27
|
+
exact = opts.fetch(:exact){true}
|
28
|
+
ignore_case = opts.fetch(:ignore_case){false}
|
29
|
+
find_format = opts.fetch(:find_format){nil}
|
30
|
+
strip_blank = opts.fetch(:strip_blank){false}
|
31
|
+
|
32
|
+
# has range?
|
33
|
+
return new scope.where(attr_values) if Range === attr_values.values.first
|
34
|
+
|
35
|
+
tokens = {}
|
36
|
+
token_i = 0
|
37
|
+
|
38
|
+
conditions = attr_values.map do |attr,value|
|
39
|
+
raise ArgumentError, "invalid range #{value} for #{attr}" if Range === value
|
40
|
+
value = value.strip if strip_blank
|
41
|
+
text = exact ? value : "%#{value}%"
|
42
|
+
col, token = attr, ":q#{token_i}"
|
43
|
+
col, token = upper(col), upper(token) if ignore_case
|
44
|
+
col = find_format.call(col) if find_format
|
45
|
+
tokens[:"q#{token_i}"] = text
|
46
|
+
token_i += 1
|
47
|
+
"#{col} #{exact ? '=' : 'LIKE'} #{token}"
|
48
|
+
end
|
49
|
+
|
50
|
+
new scope.where("( #{conditions * ') OR ('} )", tokens)
|
51
|
+
end
|
52
|
+
|
53
|
+
def greater_or_equal attr, value
|
54
|
+
new scope.where("#{attr} >= ?", value)
|
55
|
+
end
|
56
|
+
|
57
|
+
def less_or_equal attr, value
|
58
|
+
new scope.where("#{attr} <= ?", value)
|
59
|
+
end
|
60
|
+
|
61
|
+
def each &block
|
62
|
+
new scope.each(&block)
|
63
|
+
end
|
64
|
+
|
65
|
+
def named_scope name
|
66
|
+
new scope.send(name)
|
67
|
+
end
|
68
|
+
|
69
|
+
def distinct_values attr
|
70
|
+
attribute = attr.to_s.split(/\./).last || attr
|
71
|
+
scope.select(attr).uniq.map(&:"#{attribute}").uniq
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
scope.to_sql
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def upper(text)
|
81
|
+
"upper(#{text})"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|