dossier-segmenter 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +7 -0
- data/app/views/dossier/reports/cute_animals.html.haml +57 -0
- data/dossier-segmenter.gemspec +30 -0
- data/lib/dossier/segment.rb +54 -0
- data/lib/dossier/segment/chain.rb +31 -0
- data/lib/dossier/segment/definition.rb +73 -0
- data/lib/dossier/segment/rows.rb +39 -0
- data/lib/dossier/segment/summary.rb +50 -0
- data/lib/dossier/segmenter.rb +104 -0
- data/lib/dossier/segmenter/engine.rb +6 -0
- data/lib/dossier/segmenter/report.rb +34 -0
- data/lib/dossier/segmenter/version.rb +5 -0
- data/script/rails +5 -0
- data/spec/dossier/segment/chain_spec.rb +43 -0
- data/spec/dossier/segment/definition_spec.rb +84 -0
- data/spec/dossier/segment/rows_spec.rb +10 -0
- data/spec/dossier/segment/summary_spec.rb +51 -0
- data/spec/dossier/segment_spec.rb +121 -0
- data/spec/dossier/segmenter/report_spec.rb +23 -0
- data/spec/dossier/segmenter_spec.rb +171 -0
- data/spec/dummy/app/reports/cute_animals_report.rb +41 -0
- data/spec/dummy/config.ru +2 -0
- data/spec/dummy/config/application.rb +27 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/features/segmented_report_spec.rb +28 -0
- data/spec/spec_helper.rb +20 -0
- metadata +201 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 98e767c99db2057ca87c78226335c4800433514b
|
4
|
+
data.tar.gz: ae308223c209cac474b83bdcb3b94dfb1ee27ace
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b569f16bd64f42f669d9962859a1a5995c71a5d8529e540f4e3d03e84ad621e376a904b8be4a12ce11a1a14142ec8250dd3d4f36422613073193be86380ab378
|
7
|
+
data.tar.gz: 381586de07e4e0c898300c1e4f41eca9a1beec68223e1c6af0213f5bb8ac150d7127bb84c813f10ef4539e4f00a1201b0925a455ceed15eb65a09bde35abc9a4
|
data/.gitignore
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.ruby-gemset
|
6
|
+
.ruby-version
|
7
|
+
.yardoc
|
8
|
+
Gemfile.lock
|
9
|
+
InstalledFiles
|
10
|
+
_yardoc
|
11
|
+
coverage
|
12
|
+
doc/
|
13
|
+
lib/bundler/man
|
14
|
+
pkg
|
15
|
+
rdoc
|
16
|
+
spec/reports
|
17
|
+
spec/dummy/log
|
18
|
+
test/tmp
|
19
|
+
test/version_tmp
|
20
|
+
tmp
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Adam Hunter
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Dossier::Segmenter
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'dossier-segmenter'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install dossier-segmenter
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
-# TODO this should be working but ApplicationHelper
|
2
|
+
-# doesn't get included into the view context either...
|
3
|
+
-# = render_options(report)
|
4
|
+
|
5
|
+
.row-fluid
|
6
|
+
.span12
|
7
|
+
- report.segmenter.families.each do |family|
|
8
|
+
%h2= family.display_name
|
9
|
+
- family.domestics.each do |domestic|
|
10
|
+
%h3= domestic.display_name
|
11
|
+
- domestic.groups.each do |group|
|
12
|
+
%table
|
13
|
+
%thead
|
14
|
+
%tr
|
15
|
+
%th{colspan: group.headers.count}
|
16
|
+
= group.display_name
|
17
|
+
%tr
|
18
|
+
- group.headers.each do |header|
|
19
|
+
%th= report.format_header(header)
|
20
|
+
%tbody
|
21
|
+
- group.rows.each do |row|
|
22
|
+
%tr
|
23
|
+
- row.each do |value|
|
24
|
+
%td= value
|
25
|
+
%tfoot
|
26
|
+
%tr
|
27
|
+
%th Count #{commafy_number group.summary.count}
|
28
|
+
%th
|
29
|
+
%th= commafy_number group.summary.average(:cuteness), 2
|
30
|
+
%th= commafy_number group.summary.sum(:gifs)
|
31
|
+
%th
|
32
|
+
%th= number_to_dollars group.summary.average(:cost_new)
|
33
|
+
%table
|
34
|
+
%thead
|
35
|
+
%tr
|
36
|
+
%th Count
|
37
|
+
%th
|
38
|
+
%th Average
|
39
|
+
%th Sum
|
40
|
+
%th
|
41
|
+
%tbody
|
42
|
+
- family.domestics.each do |domestic|
|
43
|
+
%tr
|
44
|
+
%td= commafy_number domestic.summary.count
|
45
|
+
%td
|
46
|
+
%td= commafy_number domestic.summary.average(:cuteness), 2
|
47
|
+
%td= commafy_number domestic.summary.sum(:gifs)
|
48
|
+
%td
|
49
|
+
%td= number_to_dollars domestic.summary.average(:cost_new)
|
50
|
+
%tfoot
|
51
|
+
%tr
|
52
|
+
%th= commafy_number family.summary.count
|
53
|
+
%th
|
54
|
+
%th= commafy_number family.summary.average(:cuteness), 2
|
55
|
+
%th= commafy_number family.summary.sum(:gifs)
|
56
|
+
%th
|
57
|
+
%th= number_to_dollars family.summary.average(:cost_new)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'dossier/segmenter/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "dossier-segmenter"
|
8
|
+
spec.version = Dossier::Segmenter::VERSION
|
9
|
+
spec.authors = ["Adam Hunter"]
|
10
|
+
spec.email = ["adamhunter@me.com"]
|
11
|
+
spec.description = %q[Adds control breaks to dossier reports by defined segments based on query results.]
|
12
|
+
spec.summary = %q[Extends Dossier to have segmented report functionality.]
|
13
|
+
spec.homepage = "https://github.com/adamhunter/dossier-segmenter"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "dossier", "~> 2.8"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
+
spec.add_development_dependency "rspec-rails", "~> 2.13"
|
25
|
+
spec.add_development_dependency "rails", "~> 3.2.13"
|
26
|
+
spec.add_development_dependency "capybara", "~> 2.1"
|
27
|
+
spec.add_development_dependency "simplecov", "~> 0.7"
|
28
|
+
spec.add_development_dependency "pry", ">= 0.9.10"
|
29
|
+
spec.add_development_dependency "rake"
|
30
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Dossier
|
2
|
+
class Segment
|
3
|
+
attr_accessor :segmenter, :report, :definition, :parent, :options
|
4
|
+
|
5
|
+
def initialize(segmenter, definition, options = {})
|
6
|
+
self.segmenter = segmenter
|
7
|
+
self.report = segmenter.report
|
8
|
+
self.definition = definition
|
9
|
+
self.options = options.symbolize_keys
|
10
|
+
extend(definition.chain_module)
|
11
|
+
end
|
12
|
+
|
13
|
+
def display_name
|
14
|
+
if definition.display_name.respond_to?(:call)
|
15
|
+
definition.display_name.call(options)
|
16
|
+
else
|
17
|
+
options.fetch(definition.display_name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def group_by
|
22
|
+
options.fetch(definition.group_by)
|
23
|
+
end
|
24
|
+
|
25
|
+
def chain
|
26
|
+
@chain ||= [].tap { |collector| parent_chain(self, collector) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def key_path
|
30
|
+
chain.map(&:group_by).reverse.join('.')
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspect
|
34
|
+
"#<#{self.class.name}:#{key_path}>"
|
35
|
+
end
|
36
|
+
|
37
|
+
def summarize(row)
|
38
|
+
row.tap { summary << row }
|
39
|
+
end
|
40
|
+
|
41
|
+
def summary
|
42
|
+
@summary ||= Summary.new(headers)
|
43
|
+
end
|
44
|
+
|
45
|
+
delegate :headers, to: :segmenter
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def parent_chain(segment, collector)
|
50
|
+
collector << segment
|
51
|
+
parent_chain(segment.parent, collector) if segment.parent
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Dossier
|
2
|
+
class Segment
|
3
|
+
class Chain
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@segment_chain = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def at(index)
|
11
|
+
segment_chain.at(index)
|
12
|
+
end
|
13
|
+
alias :[] :at
|
14
|
+
|
15
|
+
def <<(segment)
|
16
|
+
last.next = segment unless last.nil?
|
17
|
+
segment.prev = last unless last.nil?
|
18
|
+
segment_chain << segment
|
19
|
+
end
|
20
|
+
|
21
|
+
def each
|
22
|
+
segment_chain.each { |segment| yield segment }
|
23
|
+
end
|
24
|
+
|
25
|
+
delegate :first, :last, :length, :empty?, to: "@segment_chain"
|
26
|
+
|
27
|
+
private
|
28
|
+
attr_reader :segment_chain
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Dossier
|
2
|
+
class Segment
|
3
|
+
class Definition
|
4
|
+
attr_accessor :segmenter_class, :report_class, :name, :group_by, :display_name, :next, :prev
|
5
|
+
attr_reader :segment_subclass
|
6
|
+
|
7
|
+
def initialize(segmenter_class, name, options = {})
|
8
|
+
self.segmenter_class = segmenter_class
|
9
|
+
self.report_class = segmenter_class.report_class
|
10
|
+
self.name = name
|
11
|
+
self.group_by = options.fetch(:group_by, name)
|
12
|
+
self.display_name = options.fetch(:display_name, name)
|
13
|
+
define_segment_subclass
|
14
|
+
end
|
15
|
+
|
16
|
+
def segment_class_name
|
17
|
+
name.to_s.classify
|
18
|
+
end
|
19
|
+
|
20
|
+
def plural_name
|
21
|
+
name.to_s.pluralize
|
22
|
+
end
|
23
|
+
|
24
|
+
def columns
|
25
|
+
[group_by, display_name_for_column].map(&:to_s).uniq
|
26
|
+
end
|
27
|
+
|
28
|
+
def next?
|
29
|
+
!!self.next
|
30
|
+
end
|
31
|
+
|
32
|
+
def prev?
|
33
|
+
!!prev
|
34
|
+
end
|
35
|
+
|
36
|
+
def chain_module
|
37
|
+
next? ? self.next.segment_module : rows_module
|
38
|
+
end
|
39
|
+
|
40
|
+
def segment_module
|
41
|
+
definition = self
|
42
|
+
Module.new do
|
43
|
+
define_method definition.plural_name do
|
44
|
+
@segments ||= segmenter.segment_options_for(self).map { |options|
|
45
|
+
definition.segment_subclass.new(segmenter, definition, options).tap do |instance|
|
46
|
+
instance.parent = self if is_a?(Dossier::Segment)
|
47
|
+
end
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def define_segment_subclass
|
56
|
+
@segment_subclass = report_class.const_set(segment_class_name, Class.new(Dossier::Segment))
|
57
|
+
end
|
58
|
+
|
59
|
+
def display_name_for_column
|
60
|
+
display_name.respond_to?(:call) ? group_by : display_name
|
61
|
+
end
|
62
|
+
|
63
|
+
def rows_module
|
64
|
+
definition = self
|
65
|
+
Module.new do
|
66
|
+
define_method :rows do
|
67
|
+
@rows ||= Rows.new(segmenter, self, definition)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Dossier
|
2
|
+
class Segment
|
3
|
+
class Rows < Dossier::Result::Formatted
|
4
|
+
attr_accessor :segmenter, :segment, :report, :definition
|
5
|
+
|
6
|
+
def initialize(segmenter, segment, definition)
|
7
|
+
self.segmenter = segmenter
|
8
|
+
self.report = segmenter.report
|
9
|
+
self.segment = segment
|
10
|
+
self.definition = definition
|
11
|
+
end
|
12
|
+
|
13
|
+
delegate :headers, to: :segmenter
|
14
|
+
delegate :length, :count, :empty?, to: :rows
|
15
|
+
|
16
|
+
def each
|
17
|
+
segmenter_data.each { |row| yield format(summarize(truncate(row))) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def inspect
|
21
|
+
"#<#{self.class.name}:@rows.count=#{rows.count}>"
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def segmenter_data
|
27
|
+
@segmenter_data ||= segmenter.data.fetch(segment.key_path)
|
28
|
+
end
|
29
|
+
|
30
|
+
def truncate(row)
|
31
|
+
row.dup.tap { |r| segmenter.header_index_map.values.sort.each_with_index { |i, j| r.delete_at(i - j) } }
|
32
|
+
end
|
33
|
+
|
34
|
+
def summarize(row)
|
35
|
+
row.tap { |r| segment.chain.each { |s| s.summarize row } }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Dossier
|
2
|
+
class Segment
|
3
|
+
class Summary
|
4
|
+
|
5
|
+
attr_reader :count, :average, :sum
|
6
|
+
|
7
|
+
def initialize(headers)
|
8
|
+
@headers = headers.map(&:to_s)
|
9
|
+
@count = 0
|
10
|
+
@sums = headers.map { 0 }
|
11
|
+
end
|
12
|
+
|
13
|
+
def <<(row)
|
14
|
+
@count += 1
|
15
|
+
row.each_with_index { |v, i| sums[i] += parse(v) }
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def sum(key)
|
20
|
+
sums.at(index_of key)
|
21
|
+
end
|
22
|
+
|
23
|
+
def average(key)
|
24
|
+
sum(key) / count
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :headers, :sums
|
30
|
+
|
31
|
+
def index_of(key)
|
32
|
+
indexes[key.to_s] ||= headers.index(key.to_s) or raise_missing_header(key)
|
33
|
+
end
|
34
|
+
|
35
|
+
def indexes
|
36
|
+
@indexes ||= {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse(value)
|
40
|
+
BigDecimal.new(value.to_s)
|
41
|
+
end
|
42
|
+
|
43
|
+
def raise_missing_header(key)
|
44
|
+
raise HeaderError.new %Q[No such header '#{key}' in headers: #{headers.join(', ')}]
|
45
|
+
end
|
46
|
+
|
47
|
+
HeaderError = Class.new(StandardError)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|