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.
Files changed (56) hide show
  1. data/.gitignore +5 -0
  2. data/.travis.yml +6 -0
  3. data/Gemfile +14 -0
  4. data/Guardfile +21 -0
  5. data/License +20 -0
  6. data/Rakefile +9 -0
  7. data/Readme.markdown +159 -0
  8. data/assets/images/sort_asc.gif +0 -0
  9. data/assets/images/sort_desc.gif +0 -0
  10. data/gitter.gemspec +28 -0
  11. data/lib/gitter.rb +13 -0
  12. data/lib/gitter/axis.rb +48 -0
  13. data/lib/gitter/base.rb +149 -0
  14. data/lib/gitter/breadcrumbs.rb +44 -0
  15. data/lib/gitter/cell.rb +11 -0
  16. data/lib/gitter/column.rb +142 -0
  17. data/lib/gitter/columns.rb +110 -0
  18. data/lib/gitter/controller.rb +19 -0
  19. data/lib/gitter/csv.rb +9 -0
  20. data/lib/gitter/driver.rb +26 -0
  21. data/lib/gitter/drivers/abstract_driver.rb +36 -0
  22. data/lib/gitter/drivers/active_record_driver.rb +84 -0
  23. data/lib/gitter/facet.rb +95 -0
  24. data/lib/gitter/filters.rb +4 -0
  25. data/lib/gitter/filters/abstract_filter.rb +94 -0
  26. data/lib/gitter/filters/block_filter.rb +16 -0
  27. data/lib/gitter/filters/column_filter.rb +50 -0
  28. data/lib/gitter/filters/select_filter.rb +43 -0
  29. data/lib/gitter/grid.rb +23 -0
  30. data/lib/gitter/header.rb +44 -0
  31. data/lib/gitter/helpers.rb +44 -0
  32. data/lib/gitter/i18n.rb +11 -0
  33. data/lib/gitter/model.rb +40 -0
  34. data/lib/gitter/pivot.rb +107 -0
  35. data/lib/gitter/pivot_grid.rb +23 -0
  36. data/lib/gitter/railtie.rb +8 -0
  37. data/lib/gitter/table.rb +149 -0
  38. data/lib/gitter/utils.rb +12 -0
  39. data/lib/gitter/version.rb +3 -0
  40. data/spec/breadcrumbs_spec.rb +24 -0
  41. data/spec/column_filter_spec.rb +80 -0
  42. data/spec/column_spec.rb +159 -0
  43. data/spec/facets_spec.rb +75 -0
  44. data/spec/grid_spec.rb +92 -0
  45. data/spec/helper_spec.rb +8 -0
  46. data/spec/i18n_spec.rb +39 -0
  47. data/spec/inputs_spec.rb +0 -0
  48. data/spec/locales/de.yml +10 -0
  49. data/spec/locales/en.yml +10 -0
  50. data/spec/range_filter_spec.rb +32 -0
  51. data/spec/scope_filter_spec.rb +12 -0
  52. data/spec/select_filter_spec.rb +22 -0
  53. data/spec/spec_helper.rb +31 -0
  54. data/spec/support/database.rb +24 -0
  55. data/spec/support/person_grid.rb +71 -0
  56. 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
@@ -0,0 +1,11 @@
1
+ module Gitter
2
+
3
+ class Cell
4
+
5
+ attr_reader :content, :html_options
6
+
7
+ def initialize content, html_options = {}
8
+ @content, @html_options = content, html_options
9
+ end
10
+ end
11
+ end
@@ -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,9 @@
1
+ module Gitter
2
+ module CSV
3
+
4
+ def to_csv(separator=',')
5
+ rows.map{|row|row.join separator}.join("\n")
6
+ end
7
+
8
+ end
9
+ end
@@ -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