rose 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,56 @@
1
+ require 'rose'
2
+ require 'active_record'
3
+
4
+ module Rose
5
+ # This class is provides ActiveRecord models the ability to run reports
6
+ class ActiveRecordAdapter < ObjectAdapter
7
+ # @see Rose::ObjectAdapter#sprout
8
+ def self.sprout(seedling, options={})
9
+ table = nil
10
+ options[:class].transaction do
11
+ table = super(seedling, options)
12
+ raise ActiveRecord::Rollback
13
+ end
14
+ table
15
+ end
16
+ end
17
+
18
+ module ActiveRecordExtensions
19
+ def self.included(base)
20
+ base.extend(ClassMethods)
21
+ end
22
+
23
+ module ClassMethods
24
+ def rose(name, options={}, &blk)
25
+ instance = Rose::Seedling.new(Rose::ActiveRecordAdapter, options.merge(:class => self))
26
+ instance.instance_eval(&blk)
27
+ register_seedling(name, instance)
28
+ end
29
+
30
+ def rose_for(name, *args)
31
+ seedlings(name).bloom(self.find(:all, *args))
32
+ end
33
+
34
+ def root_for(name, options={}, *args)
35
+ seedlings(name).photosynthesize(self.find(:all, *args), options)
36
+ end
37
+
38
+ def seedlings(name)
39
+ @seedlings ||= {}
40
+ @seedlings[name]
41
+ end
42
+
43
+ private
44
+
45
+ def register_seedling(name, instance)
46
+ @seedlings ||= {}
47
+ @seedlings[name] = Shell.new(instance)
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ # This extends ActiveRecord::Base to include Rose reporting
54
+ class ActiveRecord::Base
55
+ include Rose::ActiveRecordExtensions
56
+ end
@@ -0,0 +1,93 @@
1
+ module Rose
2
+ # An Attribute is value object used to operate on a table
3
+ class Attribute
4
+ attr_reader :method_name
5
+ attr_reader :column_name
6
+
7
+ def initialize(method_name, column_name)
8
+ @method_name = method_name
9
+ @column_name = column_name
10
+ end
11
+
12
+ def column_name
13
+ @column_name || @method_name
14
+ end
15
+
16
+ # This class defines an Attribute whose value is defined via a block
17
+ class Indirect < Attribute
18
+ attr_reader :value_block
19
+
20
+ def initialize(method_name, column_name, value_block)
21
+ super(method_name, column_name)
22
+ @value_block = value_block
23
+ end
24
+
25
+ def evaluate(item)
26
+ item.instance_eval(&@value_block)
27
+ end
28
+ end
29
+
30
+ # This class defines an Attribute whose value is defined via a method
31
+ class Direct < Attribute
32
+ def evaluate(item)
33
+ if item.respond_to?(@method_name.to_sym)
34
+ item.__send__(@method_name)
35
+ end
36
+ end
37
+ end
38
+
39
+ # This is a value object for pivot parameters
40
+ class Pivot < Indirect
41
+ alias_method :group_column, :method_name
42
+ alias_method :pivot_column, :column_name
43
+
44
+ def on(table)
45
+ table.pivot(pivot_column, :group_by => group_column, :values => value_block)
46
+ end
47
+ end
48
+
49
+ # Defines a collection of attributes
50
+ class Collection < Array
51
+ def row
52
+ inject({}) do |row, attribute|
53
+ row[attribute.column_name] = yield(attribute) if block_given?
54
+ row
55
+ end
56
+ end
57
+
58
+ def column_names
59
+ map(&:column_name)
60
+ end
61
+ end
62
+
63
+ # This is a value object for sort parameters
64
+ class Sort
65
+ attr_reader :column_name, :order, :sort_block
66
+
67
+ def initialize(column_name, order, &sort_block)
68
+ @column_name = column_name
69
+ @order = order
70
+ @sort_block = sort_block
71
+ end
72
+
73
+ def on(table)
74
+ if @sort_block
75
+ table.sort_rows_by!(nil, :order => @order) do |row|
76
+ @sort_block.call(row[@column_name])
77
+ end
78
+ else
79
+ table.sort_rows_by!(@column_name, :order => @order)
80
+ end
81
+ table
82
+ end
83
+ end
84
+
85
+ # This class defines an Indirect Attribute for rejecting table rows
86
+ class Filter < Indirect
87
+ def on(table)
88
+ table.data.reject! { |record| !@value_block.call(record) }
89
+ table
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,27 @@
1
+ module Rose
2
+ module CoreExtensions
3
+ # Validates that the given hash includes the required keys.
4
+ # If any keys are missing, ArgumentError will be raised.
5
+ #
6
+ # @param [Hash] hash the hash to validate
7
+ # @param [Array<Symbol>] required_keys the list of keys
8
+ # @raise [ArgumentError]
9
+ def require_keys(hash, *required_keys)
10
+ missing_keys = required_keys - hash.keys
11
+ raise ArgumentError, "Missing required key(s): #{missing_keys.join(', ')}" unless missing_keys.empty?
12
+ end
13
+
14
+ # Returns the values of required keys in the given hash
15
+ #
16
+ # @param [Hash] hash the hash to validate
17
+ # @param [Array<Symbol>] required_keys the list of keys
18
+ # @raise [ArgumentError] (see Rose::CoreExtensions#require_keys)
19
+ def required_values(hash, *required_keys)
20
+ require_keys(hash, *required_keys)
21
+ values = required_keys.inject([]) do |values, key|
22
+ values << hash[key]
23
+ end
24
+ required_keys.length == 1 ? values.first : values
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,130 @@
1
+ require 'rose'
2
+ require 'rose/ruport'
3
+
4
+ module Rose
5
+ # This class is provides Objects the ability to run reports
6
+ class ObjectAdapter
7
+ class << self
8
+ include CoreExtensions
9
+ end
10
+
11
+ # @param [Rose::Seedling] seedling the seedling to sprout
12
+ # @param [Hash] options the options to run with
13
+ # @option options [Array] :items (required) the items to report on
14
+ # @option options [Rose::Attribute::Collection] :attributes (nil) a row of attributes
15
+ # @option options [String] :group_column (nil) @deprecated
16
+ # @option options [String] :pivot_column (nil) @deprecated
17
+ # @option options [String] :value_block (nil) @deprecated
18
+ # @option options [String] :summary (nil) @deprecated
19
+ # @option options [String] :summary_on (nil) @deprecated
20
+ # @option options [String] :sort_by (nil) @deprecated
21
+ # @option options [String] :sort_order (nil) @deprecated
22
+ # @return [Ruport::Data::Table] the resulting table
23
+ def self.sprout(seedling, options={})
24
+ items, attributes = required_values(options, :items, :attributes)
25
+ table = Ruport::Data::RoseTable.new(:column_names => attributes.column_names)
26
+
27
+ self.rows(table, options)
28
+
29
+ if (alterations = options[:alterations])
30
+ alterations.each do |alteration|
31
+ table = alteration.on(table)
32
+ end
33
+ end
34
+
35
+ table
36
+ end
37
+
38
+ # @param [Rose::Seedling] seedling the seedling to update
39
+ # @param [Hash] options the options to run with
40
+ # @option options [Array] :items (required) an Array of items
41
+ # @option options [Hash,String] :with (required) (see Rose::Shell#photosynthesize)
42
+ def self.osmosis(seedling, options={})
43
+ hash_or_csv, items = required_values(options, :with, :items)
44
+
45
+ root, row = seedling.root, seedling.row
46
+ idy_attr = row.identity_attribute
47
+
48
+ items = case hash_or_csv
49
+ when String # CSV File
50
+ self.osmosis_from_csv(root, options.merge(
51
+ :idy_attr => idy_attr,
52
+ :csv_file => hash_or_csv,
53
+ :items => items
54
+ ))
55
+ when Hash
56
+ self.osmosis_from_hash(root, options.merge(
57
+ :idy_attr => idy_attr,
58
+ :updates => hash_or_csv,
59
+ :items => items
60
+ ))
61
+ end
62
+
63
+ self.sprout(seedling, options.merge(
64
+ :attributes => row.attributes,
65
+ :items => items
66
+ ))
67
+ end
68
+
69
+ protected
70
+
71
+ def self.rows(table, options={})
72
+ items, attributes = required_values(options, :items, :attributes)
73
+ items.each do |item|
74
+ options[:class].tap { |enforce_class| enforce_item_type(item, enforce_class) if enforce_class }
75
+ table << attributes.row { |attr| attr.evaluate(item).to_s }
76
+ end
77
+ end
78
+
79
+ def self.enforce_item_type(item, klass)
80
+ raise TypeError.new("Expected #{klass}, got #{item.class}") unless item.kind_of?(klass)
81
+ end
82
+
83
+ # @return [Array] items (see Rose::ObjectAdapter#osmosis_from_hash)
84
+ def self.osmosis_from_csv(root, options={})
85
+ idy_attr, csv_file, items = required_values(options, :idy_attr, :csv_file, :items)
86
+ updates = data_from_csv(csv_file).inject({}) do |updates, data|
87
+ updates[data.delete(idy_attr.column_name)] = data
88
+ updates
89
+ end
90
+ self.osmosis_from_hash(root, options.merge(:updates => updates))
91
+ end
92
+
93
+ # @return [Array] items the updated items
94
+ def self.osmosis_from_hash(root, options={})
95
+ idy_attr, updates, items = required_values(options, :idy_attr, :updates, :items)
96
+ finder = root.finder || auto_finder(idy_attr)
97
+ new_items = []
98
+ updates.each do |idy, update|
99
+ record = use_finder(finder, items, idy)
100
+ if record
101
+ root.updater(options[:preview]).call(record, update)
102
+ elsif creator = root.creator(options[:preview])
103
+ new_items << creator.call(idy, update)
104
+ else
105
+ next
106
+ end
107
+ end
108
+ items | new_items
109
+ end
110
+
111
+ def self.data_from_csv(csv_file)
112
+ Ruport::Data::RoseTable.load(csv_file).data.map(&:to_hash)
113
+ end
114
+
115
+ # @param [Rose::Attribute] idy_attr the attribute to find items with
116
+ # @return [Proc] a Proc that will find items with attribute evaluating to given id
117
+ def self.auto_finder(idy_attr)
118
+ lambda { |items, idy|
119
+ items.find do |item|
120
+ idy_attr.evaluate(item).to_s == idy
121
+ end
122
+ }
123
+ end
124
+
125
+ # In case subclasses want to call finders differently
126
+ def self.use_finder(finder, items, idy)
127
+ finder.call(items, idy)
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,114 @@
1
+ require 'rose/attribute'
2
+
3
+ module Rose
4
+ module Proxy
5
+ # This class is used by the DSL to collect a row of attributes
6
+ class Row
7
+ # Each attribute defines how values are generated for that column
8
+ # as well how that column is named
9
+ # @return [Rose::Attribute::Collection] A collection of attributes
10
+ attr_reader :attributes
11
+
12
+ # @return [Rose::Attribute] attribute used to determine which column
13
+ # is the id column
14
+ attr_reader :identity_attribute
15
+
16
+ def initialize
17
+ @attributes = Attribute::Collection.new
18
+ end
19
+
20
+ def column(name, &blk)
21
+ @attributes << attribute(name, &blk)
22
+ end
23
+
24
+ def identity(name, &blk)
25
+ column(name, &blk)
26
+ @identity_attribute = @attributes.last
27
+ end
28
+
29
+ def self.name_and_title(name)
30
+ case name
31
+ when Hash
32
+ name.to_a.first
33
+ else
34
+ [name, nil]
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def attribute(name, &blk)
41
+ name, title = self.class.name_and_title(name)
42
+
43
+ if block_given?
44
+ attribute = Attribute::Indirect.new(name, title, blk)
45
+ else
46
+ attribute = Attribute::Direct.new(name, title)
47
+ end
48
+ end
49
+ end
50
+
51
+ # This class is used by the DSL to collect summary attributes
52
+ class Summary < Row
53
+ attr_reader :column_name
54
+
55
+ def initialize(column_name)
56
+ super()
57
+ @column_name = column_name
58
+ end
59
+
60
+ def on(table)
61
+ rows = table.column(@column_name).uniq.inject([]) do |rows, group|
62
+ rows << @attributes.row { |attr|
63
+ gr = table.grouped_rows({@column_name => group}, attr.column_name)
64
+ attr.evaluate(gr)
65
+ }.merge(@column_name => group)
66
+ end
67
+ Ruport::Data::RoseTable.new(:column_names => [@column_name] | @attributes.column_names).tap do |table|
68
+ rows.each { |row| table << row }
69
+ end
70
+ end
71
+ end
72
+
73
+ # This class is used by the DSL to collect update attributes.
74
+ # Just like a root is the foundation of transporting water into
75
+ # a tree, a Root provides what's required to import data into a Rose
76
+ class Root
77
+ attr_reader :finder
78
+ attr_reader :updater
79
+ attr_reader :update_previewer
80
+ attr_reader :creator
81
+ attr_reader :create_previewer
82
+
83
+ def find(&blk)
84
+ @finder = blk
85
+ end
86
+
87
+ def update(&blk)
88
+ @updater = blk
89
+ end
90
+
91
+ def preview_update(&blk)
92
+ @update_previewer = blk
93
+ end
94
+
95
+ def create(&blk)
96
+ @creator = blk
97
+ end
98
+
99
+ def preview_create(&blk)
100
+ @create_previewer = blk
101
+ end
102
+
103
+ # @param [true, false] preview whether to use the update_previewer or not
104
+ def updater(preview = false)
105
+ preview ? @update_previewer : @updater
106
+ end
107
+
108
+ # @param [true, false] preview whether to use the create_previewer or not
109
+ def creator(preview = false)
110
+ preview ? @create_previewer : @creator
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,45 @@
1
+ # Enables pivoting with value columns
2
+ class Ruport::Data::RoseTable < Ruport::Data::Table
3
+ def pivot(pivot_column, options = {})
4
+ group_column = options[:group_by] ||
5
+ raise(ArgumentError, ":group_by option required")
6
+ value_column = options[:values] ||
7
+ raise(ArgumentError, ":values option required")
8
+ RosePivot.new(
9
+ self, group_column, pivot_column, nil, options
10
+ ).to_table(&value_column)
11
+ end
12
+
13
+ def grouped_rows(with, column_name)
14
+ rows_with(with).map { |row|
15
+ begin
16
+ row.send(column_name.to_sym)
17
+ rescue NoMethodError => no_method_error
18
+ nil
19
+ end
20
+ }
21
+ end
22
+ end
23
+
24
+ # Enables using value blocks for the value column
25
+ class Ruport::Data::Table::RosePivot < Ruport::Data::Table::Pivot
26
+ def to_table
27
+ result = Ruport::Data::RoseTable.new()
28
+ result.add_column(@group_column)
29
+ pivoted_columns = columns_from_pivot
30
+ pivoted_columns.each { |name| result.add_column(name) }
31
+ outer_grouping = Grouping(@table, :by => @group_column)
32
+ group_column_entries.each {|outer_group_name|
33
+ outer_group = outer_grouping[outer_group_name]
34
+ pivot_values = pivoted_columns.inject({}) do |hsh, pc|
35
+ matching_rows = outer_group.rows_with(@pivot_column => pc)
36
+ hsh[pc] = yield(matching_rows)
37
+ hsh
38
+ end
39
+ result << [outer_group_name] + pivoted_columns.map {|pc|
40
+ pivot_values[pc]
41
+ }
42
+ }
43
+ result
44
+ end
45
+ end