pivotable 0.0.2 → 0.0.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.
@@ -0,0 +1,33 @@
1
+ class Pivotable::Expression::Abstract
2
+ attr_reader :name, :model, :via
3
+
4
+ def initialize(model, name, options = {})
5
+ @name = name.to_s
6
+ @model = model
7
+ @via = process_via options[:via]
8
+
9
+ raise ArgumentError, "Could not find DB attribute for '#{@name}', please specify a :via option" if via.blank?
10
+ end
11
+
12
+ def to_select
13
+ not_implemented
14
+ end
15
+
16
+ def to_group
17
+ not_implemented
18
+ end
19
+
20
+ protected
21
+
22
+ def process_via(value)
23
+ case value
24
+ when String # Pure SQL string
25
+ value
26
+ when Symbol
27
+ model.arel_table[value]
28
+ else
29
+ model.arel_table[name.to_sym]
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,39 @@
1
+ class Pivotable::Expression::Calculation < Pivotable::Expression::Abstract
2
+
3
+ attr_reader :function
4
+
5
+ def initialize(model, name, options = {})
6
+ super
7
+ @function = options[:function] if options[:function].is_a?(Symbol)
8
+
9
+ if via.is_a?(Arel::Attribute)
10
+ raise ArgumentError, "No calculation function provided for '#{@name}'. Please provide a :function option" unless function
11
+ @via = via.send(function)
12
+ end
13
+ end
14
+
15
+ def to_select
16
+ case via
17
+ when String
18
+ "#{via} AS #{model.connection.quote(name)}"
19
+ else
20
+ via.clone.as(model.connection.quote(name))
21
+ end
22
+ end
23
+
24
+ def to_group
25
+ raise RuntimeError, "Cannot group by calculation"
26
+ end
27
+
28
+ protected
29
+
30
+ def process_via(value)
31
+ case value
32
+ when Arel::Expression
33
+ value
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ end
@@ -0,0 +1,29 @@
1
+ class Pivotable::Expression::Generic < Pivotable::Expression::Abstract
2
+
3
+ def to_select
4
+ case via
5
+ when String
6
+ "#{via} AS #{model.connection.quote(name)}"
7
+ else
8
+ via.as(name.to_sym)
9
+ end
10
+ end
11
+
12
+ def to_group
13
+ via.clone
14
+ end
15
+
16
+ protected
17
+
18
+ def process_via(value)
19
+ case value
20
+ when Symbol
21
+ model.arel_table[value]
22
+ when Arel::Attribute
23
+ value
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,5 @@
1
+ module Pivotable::Expression
2
+ autoload :Abstract, "pivotable/expression/abstract"
3
+ autoload :Generic, "pivotable/expression/generic"
4
+ autoload :Calculation, "pivotable/expression/calculation"
5
+ end
@@ -2,11 +2,6 @@
2
2
  module Pivotable::Model
3
3
  extend ActiveSupport::Concern
4
4
 
5
- included do
6
- class_attribute :_pivotable
7
- self._pivotable = {}
8
- end
9
-
10
5
  module ClassMethods
11
6
 
12
7
  # Pivotable definition for a model. Example:
@@ -14,35 +9,33 @@ module Pivotable::Model
14
9
  # class Stat < ActiveRecord::Base
15
10
  #
16
11
  # # Add your rotations
17
- # pivotable do
18
- #
19
- # rotation :overall do
20
- # sum :visits
21
- # end
22
- #
23
- # # Rotations can in herit parent definitions
24
- # rotation :by_day => :overall do
25
- # by :day
26
- # end
27
- #
12
+ # pivotable :overall do
13
+ # sum :visits
28
14
  # end
29
15
  #
30
- # # Add your custom rotations, e.g. for admins
31
- # pivotable :admin do
16
+ # # Rotations can in herit parent definitions
17
+ # pivotable :daily => :overall do
18
+ # by :day
19
+ # end
32
20
  #
33
- # rotation :overall do
34
- # sum :visits
35
- # maximum :bounce_rate
36
- # end
21
+ # # Add "namespaced" rotations, e.g. for admins
22
+ # pivotable "admin:daily" => :daily do
23
+ # maximum :bounce_rate
24
+ # end
37
25
  #
26
+ # # Can be expressed as
27
+ # pivotable :admin, :extended => [:admin, :daily] do
28
+ # sum :bot_impressions
38
29
  # end
39
30
  #
40
31
  # end
41
32
  #
42
- def pivotable(name = nil, &block)
43
- name = name.present? ? name.to_sym : :_default
44
- _pivotable[name] ||= Pivotable::Collection.new(self)
45
- _pivotable[name.to_sym].instance_eval(&block) if block
33
+ def pivotable(*tokens, &block)
34
+ name, parent = tokens.extract_options!.to_a.first
35
+ name, parent = Pivotable.name(tokens, name), Pivotable.name(parent)
36
+ raise ArgmentError, "A name must be provided" unless name
37
+
38
+ _pivotable[name] = Pivotable::Rotation.new(self, name, parent, &block) if block
46
39
  _pivotable[name]
47
40
  end
48
41
 
@@ -51,5 +44,9 @@ module Pivotable::Model
51
44
  scoped.pivot(*args)
52
45
  end
53
46
 
47
+ def _pivotable
48
+ @_pivotable ||= {}
49
+ end
50
+
54
51
  end
55
52
  end
@@ -7,13 +7,14 @@ module Pivotable::Pivoting
7
7
  # # Use custom (e.g. admin) rotation
8
8
  # Stat.pivot(:admin, :by_day)
9
9
  #
10
+ # # Same as
11
+ # Stat.pivot("admin:by_day")
12
+ #
10
13
  # # Combine it with relations & scopes
11
14
  # Stat.latest.order('some_column').pivot(:by_day).paginate :page => 1
12
15
  #
13
- def pivot(*args)
14
- rotation = args.pop
15
- scope = args.first.is_a?(Symbol) ? args.shift : nil
16
- klass.pivotable(scope)[rotation].merge(self)
16
+ def pivot(*parts)
17
+ klass.pivotable(*parts).merge(self)
17
18
  end
18
19
 
19
20
  end
@@ -1,51 +1,101 @@
1
1
  class Pivotable::Rotation
2
2
 
3
- attr_reader :collection, :name, :parent, :block, :selects, :groups, :joins, :loaded
4
- delegate :model, :to => :collection
3
+ attr_reader :model, :name, :parent, :block, :selects, :groups, :joins, :loaded
5
4
  alias :loaded? :loaded
6
5
 
7
- def initialize(collection, name, parent = nil, &block)
8
- @collection = collection
9
- @name = name.to_sym
10
- @parent = parent.to_sym if parent
11
- @selects = []
12
- @groups = []
13
- @joins = []
14
- @block = block
15
- @loaded = false
16
- end
17
-
18
- def sum(*cols)
19
- calculate :sum, *cols
6
+ def initialize(model, name, parent = nil, &block)
7
+ @model = model
8
+ @name = name
9
+ @parent = parent
10
+ @selects = []
11
+ @groups = []
12
+ @joins = []
13
+ @block = block
14
+ @loaded = false
20
15
  end
21
16
 
17
+ # Calculate minimum. Calls #calculate with :via => :minimum.
18
+ # See #calulate for examples.
22
19
  def minimum(*cols)
23
- calculate :minimum, *cols
20
+ opts = cols.extract_options!.update :function => :minimum
21
+ calculate *(cols << opts)
24
22
  end
25
23
 
24
+ # Calculate maximum. Calls #calculate with :via => :maximum.
25
+ # See #calulate for examples.
26
26
  def maximum(*cols)
27
- calculate :maximum, *cols
27
+ opts = cols.extract_options!.update :function => :maximum
28
+ calculate *(cols << opts)
28
29
  end
29
30
 
31
+ # Calculate sum. Calls #calculate with :via => :sum.
32
+ # See #calulate for examples.
33
+ def sum(*cols)
34
+ opts = cols.extract_options!.update :function => :sum
35
+ calculate *(cols << opts)
36
+ end
37
+
38
+ # Calculate sum. Calls #calculate with :via => :average.
39
+ # See #calulate for examples.
30
40
  def average(*cols)
31
- calculate :average, *cols
41
+ opts = cols.extract_options!.update :function => :average
42
+ calculate *(cols << opts)
32
43
  end
33
44
 
34
- def by(*cols)
35
- cols = columns(*cols)
36
- @selects += cols
37
- @groups += cols
45
+ # Calculate value. Examples:
46
+ #
47
+ # # Simple function
48
+ # calculate :views, :function => :sum
49
+ # # => SELECT SUM(table.views) AS views FROM table
50
+ #
51
+ # # Use a special column
52
+ # calculate :page_views, :via => :views, :function => :sum
53
+ # # => SELECT SUM(table.views) AS page_views FROM table
54
+ #
55
+ # # Use a custom SQL
56
+ # calculate :page_views, :via => "SUM(table.views)"
57
+ # # => SELECT SUM(table.views) AS page_views FROM table
58
+ #
59
+ # # Use an AREL expressions
60
+ # calculate :page_views, :via => Model.arel_table[:views].sum
61
+ # # => SELECT SUM(table.views) AS page_views FROM table
62
+ #
63
+ def calculate(*cols)
64
+ opts = cols.extract_options!
65
+ cols.each do |col|
66
+ @selects << Pivotable::Expression::Calculation.new(model, col, opts)
67
+ end
38
68
  end
39
69
 
40
- def calculate(function, *cols)
41
- columns(*cols).each do |col|
42
- col.calculate!(function)
43
- @selects << col
70
+ # Group by a column. Examples:
71
+ #
72
+ # # Simple function
73
+ # by :page_id
74
+ # # => SELECT table.page_id FROM table GROUP BY page_id
75
+ #
76
+ # # Use a special column
77
+ # by :pageid, :via => page_id
78
+ # # => SELECT table.page_id AS pageid FROM table GROUP BY page_id
79
+ #
80
+ # # Use a custom SQL
81
+ # by :page_id, :via => "page_id * 2"
82
+ # # => SELECT page_id * 2 AS page_id FROM table GROUP BY page_id * 2
83
+ #
84
+ # # Use an AREL attributes
85
+ # by :pageid, :via => Model.arel_table[:page_id]
86
+ # # => SELECT table.page_id AS pageid FROM table GROUP BY page_id
87
+ #
88
+ def by(*cols)
89
+ opts = cols.extract_options!
90
+ cols.each do |col|
91
+ expr = Pivotable::Expression::Generic.new(model, col, opts)
92
+ @selects << expr
93
+ @groups << expr
44
94
  end
45
95
  end
46
96
 
47
- def joins(*names)
48
- @joins += names
97
+ def joins(*args)
98
+ @joins += args
49
99
  end
50
100
 
51
101
  def merge(relation)
@@ -66,21 +116,12 @@ class Pivotable::Rotation
66
116
  relation
67
117
  end
68
118
 
69
- private
119
+ def load!
120
+ return if loaded?
70
121
 
71
- def load!
72
- return if loaded?
73
-
74
- instance_eval &collection[parent].block if parent
75
- instance_eval &block
76
- @loaded = true
77
- end
78
-
79
- def columns(*cols)
80
- opts = cols.extract_options!
81
- cols.map do |col|
82
- Pivotable::Column.new(model, col, opts)
83
- end
84
- end
122
+ instance_eval &model.pivotable(parent).block if parent
123
+ instance_eval &block
124
+ @loaded = true
125
+ end
85
126
 
86
127
  end
data/lib/pivotable.rb CHANGED
@@ -1,13 +1,19 @@
1
1
  require "active_support/core_ext"
2
2
  require "active_record"
3
3
  require "active_record/relation"
4
+ require "abstract"
4
5
 
5
6
  module Pivotable
6
7
  autoload :Model, "pivotable/model"
7
8
  autoload :Pivoting, "pivotable/pivoting"
8
9
  autoload :Rotation, "pivotable/rotation"
9
- autoload :Column, "pivotable/column"
10
- autoload :Collection, "pivotable/collection"
10
+ autoload :Expression, "pivotable/expression"
11
+
12
+ def self.name(*tokens)
13
+ result = tokens.flatten.map {|i| i.to_s.strip }.reject(&:blank?).join(':')
14
+ result unless result.blank?
15
+ end
16
+
11
17
  end
12
18
 
13
19
  ActiveRecord::Base.class_eval do
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pivotable
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 25
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 2
10
- version: 0.0.2
9
+ - 3
10
+ version: 0.0.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - Dimitrij Denissenko
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-07-05 00:00:00 +01:00
18
+ date: 2011-07-21 00:00:00 +01:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -75,8 +75,10 @@ extra_rdoc_files: []
75
75
  files:
76
76
  - README.markdown
77
77
  - lib/pivotable/pivoting.rb
78
- - lib/pivotable/column.rb
79
- - lib/pivotable/collection.rb
78
+ - lib/pivotable/expression/generic.rb
79
+ - lib/pivotable/expression/calculation.rb
80
+ - lib/pivotable/expression/abstract.rb
81
+ - lib/pivotable/expression.rb
80
82
  - lib/pivotable/rotation.rb
81
83
  - lib/pivotable/model.rb
82
84
  - lib/pivotable.rb
@@ -1,17 +0,0 @@
1
- class Pivotable::Collection < Hash
2
-
3
- attr_reader :model
4
-
5
- def initialize(model)
6
- @model = model
7
- super()
8
- end
9
-
10
- # Adds a new rotation to this collection
11
- def rotation(name, &block)
12
- name, parent = name.is_a?(Hash) ? name.to_a.first : [name, nil]
13
- item = Pivotable::Rotation.new(self, name, parent, &block)
14
- self[item.name] = item
15
- end
16
-
17
- end
@@ -1,35 +0,0 @@
1
- class Pivotable::Column
2
-
3
- attr_reader :attribute
4
-
5
- def initialize(model, attribute, options = {})
6
- @as = options[:as]
7
- @attribute = case attribute
8
- when Arel::Attribute
9
- attribute
10
- else
11
- model.arel_table[attribute]
12
- end
13
- raise ArgumentError, "Invalid attribute #{attribute.inspect}" unless @attribute
14
- end
15
-
16
- def name
17
- @as || attribute.name
18
- end
19
-
20
- def calculate!(function)
21
- @as ||= attribute.name
22
- @function = attribute.send(function)
23
- end
24
-
25
- def to_select
26
- select = (@function || attribute).clone
27
- select.as(@as.to_s) if @as
28
- select
29
- end
30
-
31
- def to_group
32
- attribute.clone
33
- end
34
-
35
- end