dossier-segmenter 0.9.0

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.
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
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dossier-segmenter.gemspec
4
+ gemspec
5
+
6
+ # gem 'dossier', git: 'git@github.com:adamhunter/dossier'
7
+ # gem 'dossier', path: '/usr/local/gems/dossier'
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,7 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ require File.expand_path('../spec/dummy/config/application', __FILE__)
6
+
7
+ Dummy::Application.load_tasks
@@ -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