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,107 @@
1
+ module Gitter
2
+
3
+ module Pivot
4
+
5
+ def pivot_cells *groups
6
+ opts = groups.extract_options!
7
+ sum = opts[:sum]
8
+ count = opts[:count]; count = :id if count == true
9
+
10
+ groups = [groups].flatten
11
+
12
+ cells = self.scope
13
+ groups.each{ |g| cells = cells.group(g)}
14
+ cells = cells.sum(sum) if sum
15
+ cells = cells.count(:id) if count
16
+ cells
17
+ end
18
+
19
+ def x_axis *args
20
+ if args.present?
21
+ @x_axis = Axis.new self, *args
22
+ else
23
+ @x_axis or raise ConfigurationError, 'undefined x_axis'
24
+ end
25
+ end
26
+
27
+ def y_axis *args
28
+ if args.present?
29
+ @y_axis = Axis.new self, *args
30
+ else
31
+ @y_axis or raise ConfigurationError, 'undefined y_axis'
32
+ end
33
+ end
34
+
35
+ def cell &cell
36
+ if cell
37
+ @cell ||= cell
38
+ else
39
+ @cell or raise ConfigurationError, 'undefined cell'
40
+ end
41
+ end
42
+
43
+ def drill_down *names
44
+ if names.present?
45
+ @drill_down ||= facet_for names
46
+ else
47
+ @drill_down ||= @filters.keys
48
+ end
49
+ end
50
+
51
+ def input_tags
52
+ []
53
+ end
54
+
55
+ def columns
56
+ @columns ||= x_axis.titles.map{|x|Column.new self, x}
57
+ end
58
+
59
+ def header_rows
60
+ @header_rows ||= begin
61
+ row = [''] + x_axis.titles
62
+ row = row.map{|h|Gitter::Header.new self, h}
63
+ [row]
64
+ end
65
+ end
66
+
67
+ def rows scope = self.scope
68
+ y_axis.data_titles.map do |y,y_title|
69
+ row = []
70
+ row << Gitter::Cell.new(y_title)
71
+ row += x_axis.data.map do |x|
72
+ content = cell.call data_scope, x, y
73
+ Gitter::Cell.new content
74
+ end
75
+ end
76
+ end
77
+
78
+ # returns [ down, current, ups ]
79
+ def drill_down_facets
80
+ drill = drill_down.reverse
81
+ i = drill.find_index{|f|f.selected?}
82
+ if i == 0
83
+ down = nil
84
+ current, *ups = drill[0..-1]
85
+ else
86
+ down, current, *ups = drill[i-1..-1]
87
+ end
88
+
89
+ [ down, current, ups ]
90
+ end
91
+
92
+ private
93
+ def data_scope
94
+ @data_scope ||= scope.group(x_axis.attr).group(y_axis.attr)
95
+ end
96
+
97
+ def facet_for arg
98
+ if arg.respond_to? :map
99
+ arg.map{|a|facet_for a}
100
+ else
101
+ filter = @filters[arg] or raise ConfigurationError, "unknown filter #{arg}"
102
+ Facet.new filter
103
+ end
104
+ end
105
+
106
+ end
107
+ end
@@ -0,0 +1,23 @@
1
+ require 'gitter/driver'
2
+ require 'gitter/base'
3
+ require 'gitter/pivot'
4
+ require 'gitter/breadcrumbs'
5
+ require 'gitter/csv'
6
+ require 'gitter/i18n'
7
+ require 'gitter/helpers'
8
+ require 'gitter/model'
9
+
10
+ module Gitter
11
+
12
+ class PivotGrid
13
+ include Gitter::Base
14
+ include Gitter::Driver
15
+ include Gitter::Pivot
16
+ include Gitter::Breadcrumbs
17
+ include Gitter::CSV
18
+ include Gitter::I18n
19
+ include Gitter::Helpers
20
+ include Gitter::Model
21
+ end
22
+
23
+ end
@@ -0,0 +1,8 @@
1
+ module Gitter
2
+
3
+ class Railtie < Rails::Railtie
4
+ config.after_initialize do
5
+ #ApplicationController.send :include, Gitter::Controller
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,149 @@
1
+ require 'action_view'
2
+
3
+ module Gitter
4
+
5
+ class TableCell
6
+ include ActionView::Helpers::NumberHelper
7
+
8
+
9
+ attr_reader :x, :y, :content
10
+
11
+ def initialize x, y, content
12
+ @x, @y, @content = x, y, content
13
+ end
14
+
15
+ def html opts = {}
16
+ Table.tag :td, formatted_content, opts.merge(class: "#{x} #{y}")
17
+ end
18
+
19
+ def header?
20
+ false
21
+ end
22
+
23
+ def formatted_content
24
+ number_with_delimiter content, delimiter: '.'
25
+ end
26
+ end
27
+
28
+ class TableHeaderCell < TableCell
29
+ attr_reader :content
30
+
31
+ def initialize content
32
+ @content = content
33
+ end
34
+
35
+ def html opts = {}
36
+ Table.tag :th, formatted_content, opts
37
+ end
38
+
39
+ def header?
40
+ true
41
+ end
42
+
43
+ def formatted_content
44
+ content
45
+ end
46
+
47
+ end
48
+
49
+ class Table
50
+ extend ActionView::Helpers::TagHelper
51
+ extend ActionView::Helpers::OutputSafetyHelper
52
+
53
+ def self.tag tag, content, opts = {}
54
+ opts = opts.merge(class: "#{opts[:class]} grid pivot")
55
+ content_tag tag, raw(content), opts
56
+ end
57
+
58
+ attr_reader :title, :x_axis, :y_axis, :data, :opts
59
+
60
+ # axis: responds to each, yielding [key,title] or key
61
+ # # data: hash from [x_key,y_key] to cell_array
62
+ def initialize title, x_axis, y_axis, data, opts = {}
63
+ @title, @x_axis, @y_axis, @data, @opts = title, x_axis, y_axis, data, opts
64
+ @cells = data.dup
65
+ if label = opts[:show_sums]
66
+ add_sums
67
+ @x_axis = add_sum_label_to_axis @x_axis, label
68
+ @y_axis = add_sum_label_to_axis @y_axis, label
69
+ end
70
+ end
71
+
72
+ def empty?
73
+ data.empty?
74
+ end
75
+
76
+ def rows
77
+ @rows ||= begin
78
+ rows = []
79
+ rows << x_header if x_header
80
+
81
+ rows + (y_axis||[nil]).map do |y,y_title|
82
+ row = (x_axis||[nil]).map do |x,x_title|
83
+ cell = @cells[cell_key(x,y)]
84
+ cell = yield cell, x, y if block_given?
85
+ TableCell.new x, y, cell
86
+ end
87
+ row.unshift TableHeaderCell.new(y_title||y) if y_axis
88
+ row
89
+ end
90
+ end
91
+ end
92
+
93
+ def html
94
+ @html ||= begin
95
+ h = rows.map do |row|
96
+ Table.tag :tr, (row.map{|cell| cell.html} * "\n")
97
+ end * "\n"
98
+ Table.tag :table, h
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def x_header
105
+ @x_header ||= begin
106
+ if x_axis
107
+ h = x_axis.map{|key,title| TableHeaderCell.new(title||key) }
108
+ h.unshift TableHeaderCell.new('') if y_axis
109
+ h
110
+ else
111
+ nil
112
+ end
113
+ end
114
+ end
115
+
116
+ def cell_key x, y
117
+ if x.nil? || y.nil?
118
+ x.nil? ? y : x
119
+ else
120
+ [x,y]
121
+ end
122
+ end
123
+
124
+ def add_sums
125
+ xsums, ysums = {}, {}
126
+ sum = 0
127
+ @cells.each do |key,value|
128
+ x, y = *key
129
+ xsums[y] = (xsums[y]||0) + value
130
+ ysums[x] = (ysums[x]||0) + value
131
+ sum += value
132
+ end
133
+ xsums.each{|y,sum| @cells[cell_key(:sum,y)] = sum}
134
+ ysums.each{|x,sum| @cells[cell_key(x,:sum)] = sum}
135
+ @cells[[:sum,:sum]] = sum
136
+ @cells
137
+ end
138
+
139
+ def add_sum_label_to_axis axis, label = nil
140
+ label = 'Sum' unless String === label
141
+ case axis
142
+ when Array then axis + [[:sum, label]]
143
+ when Hash then axis.merge(:sum => label)
144
+ else nil;
145
+ end
146
+ end
147
+
148
+ end
149
+ end
@@ -0,0 +1,12 @@
1
+ module Gitter
2
+
3
+ module Utils
4
+ # dirty hack to avoid rails' sorted query in url
5
+ def url_for params
6
+ p = params.dup
7
+ query = p.map{|key, value| value.to_query(key) } * '&'
8
+ "#{h.url_for({})}?#{query}"
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,3 @@
1
+ module Gitter
2
+ VERSION = "1.1.3"
3
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ include Gitter
4
+
5
+ describe Grid do
6
+
7
+ it 'should handle one breadcrumb' do
8
+ g = PersonGrid.new :name => 'bla'
9
+ g.breadcrumbs.should == { 'Name' => 'bla' }
10
+ end
11
+
12
+ it 'should handle breadcrumb with label' do
13
+ g = PersonGrid.new :name3 => 'bla'
14
+ g.breadcrumbs.should == { 'Three' => 'bla' }
15
+ end
16
+
17
+ it 'should handle many breadcrumbs' do
18
+ g = PersonGrid.new :name => 'Mike', :surname => 'Miller'
19
+ g.breadcrumbs.should == { 'Name' => 'Mike', 'Surname' => 'Miller' }
20
+
21
+ g = PersonGrid.new :surname => 'Miller', :name => 'Mike'
22
+ g.breadcrumbs.should == { 'Surname' => 'Miller', 'Name' => 'Mike' }
23
+ end
24
+ end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ include Persons
4
+
5
+ describe Gitter do
6
+
7
+ context 'column filter' do
8
+ check_include Max, :name => 'Max'
9
+ check_include Lisa, :surname => 'Adult'
10
+ check_include John, Dana, :surname => 'Twen'
11
+ end
12
+
13
+ context 'column filter with :column' do
14
+ check_include Max, :name2 => 'Max'
15
+ end
16
+
17
+ context 'column filter with instance ignore_case' do
18
+ check_include [], :name => 'max'
19
+ check_include [], :name => 'max', :ignore_case => false
20
+ check_include Max, :name => 'max', :ignore_case => true
21
+ end
22
+
23
+ context 'column filter with instance inexact' do
24
+ check_include [], :name => 'ax'
25
+ check_include [], :name => 'ax', :exact => true
26
+ check_include Max, :name => 'ax', :exact => false
27
+ end
28
+
29
+ context 'column filter with many columns' do
30
+ check_include Max, :any_name => 'Max'
31
+ check_include Max, :any_name => 'Kid'
32
+ end
33
+
34
+ context 'column filter with many columns with ignore_case' do
35
+ check_include [], :any_name => 'max'
36
+ check_include [], :any_name => 'ax', :exact => true
37
+ check_include Max, :any_name => 'ax', :exact => false
38
+ check_include [], :any_name => 'id'
39
+ check_include [], :any_name => 'id', :exact => true
40
+ check_include Max, :any_name => 'id', :exact => false
41
+ end
42
+
43
+ context 'column filter with many columns with inexact' do
44
+ check_include [], :any_name => 'max'
45
+ check_include [], :any_name => 'max', :ignore_case => false
46
+ check_include Max, :any_name => 'max', :ignore_case => true
47
+ check_include [], :any_name => 'kid'
48
+ check_include [], :any_name => 'kid', :ignore_case => false
49
+ check_include Max, :any_name => 'kid', :ignore_case => true
50
+ end
51
+
52
+ context 'column filter with ignore_case' do
53
+ check_include Max, :name_ignore => 'max'
54
+ check_include Max, :name_ignore => 'Max'
55
+
56
+ check_include [], :name_ignore => 'max', :ignore_case => false
57
+ check_include Max, :name_ignore => 'Max', :ignore_case => false
58
+
59
+ check_include Max, :name_ignore => 'max', :ignore_case => true
60
+ check_include Max, :name_ignore => 'Max', :ignore_case => true
61
+ end
62
+
63
+ context 'column filter with instance inexact' do
64
+ check_include Max, :name_inexact => 'ax'
65
+ check_include Max, :name_inexact => 'Max'
66
+
67
+ check_include [], :name_inexact => 'ax', :exact => true
68
+ check_include Max, :name_inexact => 'Max', :exact => true
69
+
70
+ check_include Max, :name_inexact => 'ax', :exact => false
71
+ check_include Max, :name_inexact => 'Max', :exact => false
72
+ end
73
+
74
+ context 'search' do
75
+ check_include Max, :search_name => 'Max'
76
+ check_include Max, :search_name => 'max'
77
+ check_include Max, :search_name => 'ax'
78
+ check_include [], :search_name => 'Kid'
79
+ end
80
+ end
@@ -0,0 +1,159 @@
1
+ require 'spec_helper'
2
+
3
+ include Gitter
4
+
5
+ describe Column do
6
+ it 'should have name and header' do
7
+ g = PersonGrid.new
8
+ col_spec = g.columns.detect_name :name
9
+ col_spec.name.should == :name
10
+
11
+ col= g.columns.detect_name :name
12
+ col.name.should == :name
13
+ col.header.should == 'Name'
14
+ end
15
+
16
+ it 'should have settable header' do
17
+ g = PersonGrid.new
18
+ col_spec = g.columns.detect_name :full_name
19
+ col_spec.name.should == :full_name
20
+
21
+ col = g.columns.detect_name :full_name
22
+ col.name.should == :full_name
23
+ col.header.should == 'Full name'
24
+ end
25
+
26
+ it 'should have a ColumnSpec' do
27
+ g = PersonGrid.new
28
+ col= g.columns.detect_name :name
29
+ col.spec.class.should == ColumnSpec
30
+ end
31
+
32
+ it 'should have settable attribute' do
33
+ class Foo < Gitter::Grid
34
+ scope do Person.scoped end
35
+ column :name1, :column => :name
36
+ end
37
+ g = Foo.new
38
+ col_spec = g.columns.detect_name :name1
39
+ col_spec.name.should == :name1
40
+
41
+ col = g.columns.detect_name :name1
42
+ col.name.should == :name1
43
+ col.header.should == 'Name1'
44
+ end
45
+
46
+ it 'should have headers' do
47
+ g = PersonGrid.new
48
+ g.headers.should == [ 'Name', 'Full name', 'Job Title' ]
49
+ end
50
+
51
+ it 'should have rows' do
52
+ g = PersonGrid.new
53
+ g.rows.size.should == 7
54
+
55
+ g = PersonGrid.new :order => :name
56
+ g.rows.size.should == 7
57
+ g.rows.should == [
58
+ ["Dana", "Dana Twen", "teacher"],
59
+ ["Dick", "Dick Teeny", "student"],
60
+ ["Joe", "Joe Teen", "student"],
61
+ ["John", "John Twen", "teacher"],
62
+ ["Lisa", "Lisa Adult", "dentist"],
63
+ ["Max", "Max Kid", "student"],
64
+ ["Tina", "Tina Child", "student"]
65
+ ]
66
+ end
67
+
68
+ it 'should order columns' do
69
+ g = PersonGrid.new :order => :profession
70
+ g.rows.size.should == 7
71
+ g.rows.map{|r|r.last}.should == [
72
+ "dentist",
73
+ "student",
74
+ "student",
75
+ "student",
76
+ "student",
77
+ "teacher",
78
+ "teacher"
79
+ ]
80
+ end
81
+
82
+ it 'should order columns ascending' do
83
+ g = PersonGrid.new :order => :profession, :desc => false
84
+ g.rows.size.should == 7
85
+ g.rows.map{|r|r.last}.should == [
86
+ "dentist",
87
+ "student",
88
+ "student",
89
+ "student",
90
+ "student",
91
+ "teacher",
92
+ "teacher"
93
+ ]
94
+ end
95
+
96
+ it 'should order columns descending' do
97
+ g = PersonGrid.new :order => :profession, :desc => true
98
+ g.rows.size.should == 7
99
+ g.rows.map{|r|r.last}.should == [
100
+ "teacher",
101
+ "teacher",
102
+ "student",
103
+ "student",
104
+ "student",
105
+ "student",
106
+ "dentist"
107
+ ]
108
+ end
109
+
110
+ it 'should order columns descending with given descend' do
111
+ g = PersonGrid.new :order => :profession, :desc => 'profession DESC'
112
+ g.rows.size.should == 7
113
+ g.rows.map{|r|r.last}.should == [
114
+ "teacher",
115
+ "teacher",
116
+ "student",
117
+ "student",
118
+ "student",
119
+ "student",
120
+ "dentist"
121
+ ]
122
+ end
123
+
124
+ it 'should order complex columns' do
125
+ g = PersonGrid.new :order => :full_name
126
+ g.rows.size.should == 7
127
+ g.rows.should == [
128
+ ["Dana", "Dana Twen", "teacher"],
129
+ ["Dick", "Dick Teeny", "student"],
130
+ ["Joe", "Joe Teen", "student"],
131
+ ["John", "John Twen", "teacher"],
132
+ ["Lisa", "Lisa Adult", "dentist"],
133
+ ["Max", "Max Kid", "student"],
134
+ ["Tina", "Tina Child", "student"]
135
+ ]
136
+ end
137
+
138
+ it 'should order complex columns descending' do
139
+ g = PersonGrid.new :order => :full_name, :desc => true
140
+ g.rows.size.should == 7
141
+ g.rows.should == [
142
+ ["Tina", "Tina Child", "student"],
143
+ ["Max", "Max Kid", "student"],
144
+ ["Lisa", "Lisa Adult", "dentist"],
145
+ ["John", "John Twen", "teacher"],
146
+ ["Joe", "Joe Teen", "student"],
147
+ ["Dick", "Dick Teeny", "student"],
148
+ ["Dana", "Dana Twen", "teacher"]
149
+ ]
150
+ end
151
+
152
+ it 'should raise error for unknown order column' do
153
+ expect {
154
+ PersonGrid.new :order => :bla
155
+ }.to raise_error(
156
+ ArgumentError, /order/
157
+ )
158
+ end
159
+ end