reportable 1.0.3 → 1.1.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.
Files changed (35) hide show
  1. data/HISTORY.md +10 -0
  2. data/README.md +49 -10
  3. data/Rakefile +0 -1
  4. data/generators/reportable_jquery_flot_assets/reportable_jquery_flot_assets_generator.rb +37 -0
  5. data/generators/reportable_jquery_flot_assets/templates/NOTES +7 -0
  6. data/generators/reportable_jquery_flot_assets/templates/excanvas.min.js +1 -0
  7. data/generators/reportable_jquery_flot_assets/templates/jquery.flot.min.js +1 -0
  8. data/generators/reportable_migration/reportable_migration_generator.rb +35 -4
  9. data/generators/reportable_migration/templates/{migration.erb → migration.rb} +7 -7
  10. data/generators/reportable_raphael_assets/reportable_raphael_assets_generator.rb +42 -0
  11. data/generators/reportable_raphael_assets/templates/NOTES +6 -0
  12. data/generators/reportable_raphael_assets/templates/g.line.min.js +7 -0
  13. data/generators/reportable_raphael_assets/templates/g.raphael.min.js +7 -0
  14. data/generators/reportable_raphael_assets/templates/raphael.min.js +113 -0
  15. data/lib/saulabs/reportable.rb +7 -0
  16. data/lib/saulabs/reportable/config.rb +55 -0
  17. data/lib/saulabs/reportable/cumulated_report.rb +2 -0
  18. data/lib/saulabs/reportable/grouping.rb +2 -2
  19. data/lib/saulabs/reportable/railtie.rb +26 -0
  20. data/lib/saulabs/reportable/report.rb +3 -0
  21. data/lib/saulabs/reportable/report_cache.rb +13 -1
  22. data/lib/saulabs/reportable/report_tag_helper.rb +162 -0
  23. data/lib/saulabs/reportable/reporting_period.rb +2 -3
  24. data/lib/saulabs/reportable/result_set.rb +40 -0
  25. data/rails/init.rb +3 -1
  26. data/spec/boot.rb +4 -7
  27. data/spec/classes/grouping_spec.rb +1 -1
  28. data/spec/classes/report_cache_spec.rb +85 -0
  29. data/spec/db/schema.rb +6 -6
  30. data/spec/other/report_method_spec.rb +27 -3
  31. data/spec/other/report_tag_helper_spec.rb +118 -0
  32. data/spec/spec_helper.rb +3 -7
  33. metadata +54 -18
  34. data/lib/saulabs/reportable/sparkline_tag_helper.rb +0 -62
  35. data/spec/other/sparkline_tag_helper_spec.rb +0 -64
@@ -1,3 +1,5 @@
1
+ require 'saulabs/reportable/report'
2
+
1
3
  module Saulabs
2
4
 
3
5
  module Reportable
@@ -66,7 +66,7 @@ module Saulabs
66
66
  if @identifier == :week
67
67
  parts = [db_string[0..3], db_string[4..5]].map(&:to_i)
68
68
  else
69
- db_string.split('/').map(&:to_i)
69
+ db_string.split(@identifier == :day ? '-' : '/').map(&:to_i)
70
70
  end
71
71
  end
72
72
 
@@ -99,7 +99,7 @@ module Saulabs
99
99
  when :hour
100
100
  "DATE_FORMAT(#{date_column}, '%Y/%m/%d/%H')"
101
101
  when :day
102
- "DATE_FORMAT(#{date_column}, '%Y/%m/%d')"
102
+ "DATE(#{date_column})"
103
103
  when :week
104
104
  "YEARWEEK(#{date_column}, 3)"
105
105
  when :month
@@ -0,0 +1,26 @@
1
+ require 'saulabs/reportable'
2
+ require 'rails'
3
+
4
+ module Saulabs
5
+
6
+ module Reportable
7
+
8
+ class Railtie < Rails::Railtie
9
+
10
+ GEM_ROOT = File.join(File.dirname(__FILE__), '..', '..', '..')
11
+
12
+ initializer 'saulabs.reportable.initialization' do
13
+ require File.join(GEM_ROOT, 'rails', 'init')
14
+ end
15
+
16
+ generators do
17
+ require File.join(GEM_ROOT, 'generators', 'reportable_migration', 'reportable_migration_generator')
18
+ require File.join(GEM_ROOT, 'generators', 'reportable_raphael_assets', 'reportable_raphael_assets_generator')
19
+ require File.join(GEM_ROOT, 'generators', 'reportable_jquery_flot_assets', 'reportable_jquery_flot_assets_generator')
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -1,3 +1,6 @@
1
+ require 'saulabs/reportable/grouping'
2
+ require 'saulabs/reportable/report_cache'
3
+
1
4
  module Saulabs
2
5
 
3
6
  module Reportable
@@ -1,3 +1,6 @@
1
+ require 'saulabs/reportable/reporting_period'
2
+ require 'saulabs/reportable/result_set'
3
+
1
4
  module Saulabs
2
5
 
3
6
  module Reportable
@@ -9,6 +12,15 @@ module Saulabs
9
12
 
10
13
  set_table_name :reportable_cache
11
14
 
15
+ validates_presence_of :model_name
16
+ validates_presence_of :report_name
17
+ validates_presence_of :grouping
18
+ validates_presence_of :aggregation
19
+ validates_presence_of :value
20
+ validates_presence_of :reporting_period
21
+
22
+ attr_accessible :model_name, :report_name, :grouping, :aggregation, :value, :reporting_period, :conditions
23
+
12
24
  self.skip_time_zone_conversion_for_attributes = [:reporting_period]
13
25
 
14
26
  # Clears the cache for the specified +klass+ and +report+
@@ -84,7 +96,7 @@ module Saulabs
84
96
  if options[:live_data]
85
97
  result << [current_reporting_period.date_time, find_value(new_data, current_reporting_period)]
86
98
  end
87
- result
99
+ Saulabs::Reportable::ResultSet.new(result, report.klass.name, report.name)
88
100
  end
89
101
 
90
102
  def self.find_value(data, reporting_period)
@@ -0,0 +1,162 @@
1
+ require 'saulabs/reportable/config'
2
+
3
+ module Saulabs
4
+
5
+ module Reportable
6
+
7
+ module ReportTagHelper
8
+
9
+ # Renders a sparkline with the given data using the google drawing api.
10
+ #
11
+ # @param [Array<Array<DateTime, Float>>] data
12
+ # an array of report data as returned by {Saulabs::Reportable::Report#run}
13
+ # @param [Hash] options
14
+ # options for the sparkline
15
+ #
16
+ # @option options [Fixnum] :width (300)
17
+ # the width of the generated image
18
+ # @option options [Fixnum] :height (34)
19
+ # the height of the generated image
20
+ # @option options [String] :line_color ('0077cc')
21
+ # the line color of the generated image
22
+ # @option options [String] :fill_color ('e6f2fa')
23
+ # the fill color of the generated image
24
+ # @option options [Array<Symbol>] :labels ([])
25
+ # the axes to render lables for (Array of +:x+, +:y+, +:r+, +:t+; this is x axis, y axis, right, top)
26
+ # @option options [String] :alt ('')
27
+ # the alt attribute for the generated image
28
+ # @option options [String] :title ('')
29
+ # the title attribute for the generated image
30
+ #
31
+ # @return [String]
32
+ # an image tag showing a sparkline for the passed +data+
33
+ #
34
+ # @example Rendering a sparkline tag for report data
35
+ #
36
+ # <%= google_report_tag(User.registrations_report, :width => 200, :height => 100, :color => '000') %>
37
+ #
38
+ def google_report_tag(data, options = {})
39
+ options.reverse_merge!(Config.google_options)
40
+ data = data.collect { |d| d[1] }
41
+ labels = ''
42
+ unless options[:labels].empty?
43
+ chxr = {}
44
+ options[:labels].each_with_index do |l, i|
45
+ chxr[l] = "#{i}," + ([:x, :t].include?(l) ? "0,#{data.length}" : "#{[data.min, 0].min},#{data.max}")
46
+ end
47
+ labels = "&chxt=#{options[:labels].map(&:to_s).join(',')}&chxr=#{options[:labels].collect{|l| chxr[l]}.join('|')}"
48
+ end
49
+ title = ''
50
+ unless options[:title].blank?
51
+ title = "&chtt=#{options[:title]}"
52
+ end
53
+ image_tag(
54
+ "http://chart.apis.google.com/chart?cht=ls&chs=#{options[:width]}x#{options[:height]}&chd=t:#{data.join(',')}&chco=#{options[:line_color]}&chm=B,#{options[:fill_color]},0,0,0&chls=1,0,0&chds=#{data.min},#{data.max}#{labels}#{title}",
55
+ :alt => options[:alt],
56
+ :title => options[:title]
57
+ )
58
+ end
59
+
60
+
61
+ # Renders a sparkline with the given data using Raphael.
62
+ #
63
+ # @param [Array<Array<DateTime, Float>>] data
64
+ # an array of report data as returned by {Saulabs::Reportable::Report#run}
65
+ # @param [Hash] options
66
+ # options for width, height, the dom id and the format
67
+ # @param [Hash] raphael_options
68
+ # options that are passed directly to Raphael as JSON
69
+ #
70
+ # @option options [Fixnum] :width (300)
71
+ # the width of the generated graph
72
+ # @option options [Fixnum] :height (34)
73
+ # the height of the generated graph
74
+ # @option options [Array<Symbol>] :dom_id ("reportable_#{Time.now.to_i}")
75
+ # the dom id of the generated div
76
+ #
77
+ # @return [String]
78
+ # an div tag and the javascript code showing a sparkline for the passed +data+
79
+ #
80
+ # @example Rendering a sparkline tag for report data
81
+ #
82
+ # <%= raphael_report_tag(User.registrations_report, { :width => 200, :height => 100, :format => 'div(100).to_i' }, { :vertical_label_unit => 'registrations' }) %>
83
+ #
84
+ def raphael_report_tag(data, options = {}, raphael_options = {})
85
+ @__raphael_report_tag_count ||= -1
86
+ @__raphael_report_tag_count += 1
87
+ default_dom_id = "#{data.model_name.downcase}_#{data.report_name}#{@__raphael_report_tag_count > 0 ? @__raphael_report_tag_count : ''}"
88
+ options.reverse_merge!(Config.raphael_options.slice(:width, :height, :format))
89
+ options.reverse_merge!(:dom_id => default_dom_id)
90
+ raphael_options.reverse_merge!(Config.raphael_options.except(:width, :height, :format))
91
+ %Q{<div id="#{options[:dom_id]}" style="width:#{options[:width]}px;height:#{options[:height]}px;"></div>
92
+ <script type="text\/javascript" charset="utf-8">
93
+ var graph = Raphael('#{options[:dom_id]}');
94
+ graph.g.linechart(
95
+ -10, 4, #{options[:width]}, #{options[:height]},
96
+ #{(0..data.size).to_a.to_json},
97
+ #{data.map { |d| eval options[:format], d[1].send(:binding) }.to_json},
98
+ #{raphael_options.to_json}
99
+ ).hover(function() {
100
+ this.disc = graph.g.disc(this.x, this.y, 3).attr({fill: "#{options[:hover_fill_color]}", stroke: '#{options[:hover_line_color]}' }).insertBefore(this);
101
+ this.flag = graph.g.flag(this.x, this.y, this.value || "0", 0).insertBefore(this);
102
+ if (this.x + this.flag.getBBox().width > this.paper.width) {
103
+ this.flag.rotate(-180);
104
+ this.flag.translate(-this.flag.getBBox().width, 0);
105
+ this.flag.items[1].rotate(180);
106
+ this.flag.items[1].translate(-5, 0);
107
+ }
108
+ }, function() {
109
+ this.disc.remove();
110
+ this.flag.remove();
111
+ });
112
+ </script>}
113
+ end
114
+
115
+ # Renders a sparkline with the given data using the jquery flot plugin.
116
+ #
117
+ # @param [Array<Array<DateTime, Float>>] data
118
+ # an array of report data as returned by {Saulabs::Reportable::Report#run}
119
+ # @param [Hash] options
120
+ # options for width, height, the dom id and the format
121
+ # @param [Hash] flot_options
122
+ # options that are passed directly to Raphael as JSON
123
+ #
124
+ # @option options [Fixnum] :width (300)
125
+ # the width of the generated graph
126
+ # @option options [Fixnum] :height (34)
127
+ # the height of the generated graph
128
+ # @option options [Array<Symbol>] :dom_id ("reportable_#{Time.now.to_i}")
129
+ # the dom id of the generated div
130
+ #
131
+ # @return [String]
132
+ # an div tag and the javascript code showing a sparkline for the passed +data+
133
+ #
134
+ # @example Rendering a sparkline tag for report data
135
+ #
136
+ # <%= flot_report_tag(User.registrations_report) %>
137
+ #
138
+
139
+ def flot_report_tag(data, options = {}, flot_options = {})
140
+ @__flot_report_tag_count ||= -1
141
+ @__flot_report_tag_count += 1
142
+ default_dom_id = "#{data.model_name.downcase}_#{data.report_name}#{@__flot_report_tag_count > 0 ? @__flot_report_tag_count : ''}"
143
+ options.reverse_merge!(Config.flot_options.slice(:width, :height, :format))
144
+ options.reverse_merge!(:dom_id => default_dom_id)
145
+ flot_options.reverse_merge!(Config.flot_options.except(:width, :height, :format))
146
+ %Q{<div id="#{options[:dom_id]}" style="width:#{options[:width]}px;height:#{options[:height]}px;"></div>
147
+ <script type="text\/javascript" charset="utf-8">
148
+ $(function() {
149
+ var set = #{data.map{|d| d[1] }.to_json},
150
+ data = [];
151
+ for (var i = 0; i < set.length; i++) {
152
+ data.push([i, set[i]]);
153
+ }
154
+ $.plot($('##{options[:dom_id]}'), [data], #{flot_options.to_json});
155
+ });
156
+ </script>}
157
+ end
158
+
159
+ end
160
+ end
161
+
162
+ end
@@ -71,7 +71,7 @@ module Saulabs
71
71
  #
72
72
  def self.from_db_string(grouping, db_string)
73
73
  parts = grouping.date_parts_from_db_string(db_string)
74
- result = case grouping.identifier
74
+ case grouping.identifier
75
75
  when :hour
76
76
  self.new(grouping, DateTime.new(parts[0], parts[1], parts[2], parts[3], 0, 0))
77
77
  when :day
@@ -81,7 +81,6 @@ module Saulabs
81
81
  when :month
82
82
  self.new(grouping, Date.new(parts[0], parts[1], 1))
83
83
  end
84
- result
85
84
  end
86
85
 
87
86
  # Gets the next reporting period.
@@ -112,7 +111,7 @@ module Saulabs
112
111
  #
113
112
  def ==(other)
114
113
  if other.is_a?(Saulabs::Reportable::ReportingPeriod)
115
- @date_time.to_s == other.date_time.to_s && @grouping.identifier.to_s == other.grouping.identifier.to_s
114
+ @date_time == other.date_time && @grouping.identifier == other.grouping.identifier
116
115
  elsif other.is_a?(Time) || other.is_a?(DateTime)
117
116
  @date_time == parse_date_time(other)
118
117
  else
@@ -0,0 +1,40 @@
1
+ module Saulabs
2
+
3
+ module Reportable
4
+
5
+ # A result set as it is returned by the report methods.
6
+ # This is basically a subclass of +Array+ that adds two
7
+ # attributes, +model_name+ and +report_name+ that store
8
+ # the name of the model and the report the result set
9
+ # was generated from.
10
+ #
11
+ class ResultSet < ::Array
12
+
13
+ # the name of the model the result set is based on
14
+ #
15
+ attr_reader :model_name
16
+
17
+ # the name of the report the result is based on
18
+ #
19
+ attr_reader :report_name
20
+
21
+ # Initializes a new result set.
22
+ #
23
+ # @param [Array] array
24
+ # the array that is the actual result
25
+ # @param [String] model_name
26
+ # the name of the model the result set is based on
27
+ # @param [String] report_name
28
+ # the name of the report the result is based on
29
+ #
30
+ def initialize(array, model_name, report_name)
31
+ super(array)
32
+ @model_name = model_name
33
+ @report_name = report_name.to_s
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -1,9 +1,11 @@
1
+ require 'action_view'
1
2
  require 'saulabs/reportable'
3
+ require 'saulabs/reportable/report_tag_helper'
2
4
 
3
5
  ActiveRecord::Base.class_eval do
4
6
  include Saulabs::Reportable
5
7
  end
6
8
 
7
9
  ActionView::Base.class_eval do
8
- include Saulabs::Reportable::SparklineTagHelper
10
+ include Saulabs::Reportable::ReportTagHelper
9
11
  end
@@ -1,14 +1,11 @@
1
1
  plugin_root = File.join(File.dirname(__FILE__), '..')
2
2
 
3
- gem 'rails'
4
- require 'active_record'
5
- require 'active_support'
6
- require 'action_controller'
7
- require 'action_view'
8
-
9
3
  $:.unshift "#{plugin_root}/lib"
10
4
 
11
- RAILS_ROOT = File.expand_path(File.dirname(__FILE__) + '/../')
5
+ Bundler.require
6
+ require 'initializer'
7
+
8
+ RAILS_ROOT = File.expand_path(File.dirname(__FILE__) + '/../') unless defined?(RAILS_ROOT)
12
9
  Rails::Initializer.run(:set_load_path)
13
10
  Rails::Initializer.run(:set_autoload_paths)
14
11
  Rails::Initializer.run(:initialize_time_zone) do |config|
@@ -23,7 +23,7 @@ describe Saulabs::Reportable::Grouping do
23
23
  end
24
24
 
25
25
  it 'should use DATE_FORMAT with format string "%Y/%m/%d" for grouping :day' do
26
- Saulabs::Reportable::Grouping.new(:day).send(:to_sql, 'created_at').should == "DATE_FORMAT(created_at, '%Y/%m/%d')"
26
+ Saulabs::Reportable::Grouping.new(:day).send(:to_sql, 'created_at').should == "DATE(created_at)"
27
27
  end
28
28
 
29
29
  it 'should use YEARWEEK with mode 3 for grouping :week' do
@@ -6,6 +6,91 @@ describe Saulabs::Reportable::ReportCache do
6
6
  @report = Saulabs::Reportable::Report.new(User, :registrations, :limit => 10)
7
7
  end
8
8
 
9
+ describe 'validations' do
10
+
11
+ before do
12
+ @report_cache = Saulabs::Reportable::ReportCache.new(
13
+ :model_name => User.name,
14
+ :report_name => 'registrations',
15
+ :grouping => 'date',
16
+ :aggregation => 'count',
17
+ :value => 1.0,
18
+ :reporting_period => '2070/03/23'
19
+ )
20
+ end
21
+
22
+ it 'should succeed when all required attributes are set' do
23
+ @report_cache.should be_valid
24
+ end
25
+
26
+ it 'should not succeed when no model_name is set' do
27
+ @report_cache.model_name = nil
28
+
29
+ @report_cache.should_not be_valid
30
+ end
31
+
32
+ it 'should not succeed when a blank model_name is set' do
33
+ @report_cache.model_name = ''
34
+
35
+ @report_cache.should_not be_valid
36
+ end
37
+
38
+ it 'should not succeed when no report_name is set' do
39
+ @report_cache.report_name = nil
40
+
41
+ @report_cache.should_not be_valid
42
+ end
43
+
44
+ it 'should not succeed when a blank report_name is set' do
45
+ @report_cache.report_name = ''
46
+
47
+ @report_cache.should_not be_valid
48
+ end
49
+
50
+ it 'should not succeed when no grouping is set' do
51
+ @report_cache.grouping = nil
52
+
53
+ @report_cache.should_not be_valid
54
+ end
55
+
56
+ it 'should not succeed when a blank grouping is set' do
57
+ @report_cache.grouping = ''
58
+
59
+ @report_cache.should_not be_valid
60
+ end
61
+
62
+ it 'should not succeed when no aggregation is set' do
63
+ @report_cache.aggregation = nil
64
+
65
+ @report_cache.should_not be_valid
66
+ end
67
+
68
+ it 'should not succeed when a blank aggregation is set' do
69
+ @report_cache.aggregation = ''
70
+
71
+ @report_cache.should_not be_valid
72
+ end
73
+
74
+ it 'should not succeed when no value is set' do
75
+ @report_cache.value = nil
76
+
77
+ @report_cache.should_not be_valid
78
+ end
79
+
80
+ it 'should not succeed when no reporting_period is set' do
81
+ @report_cache.reporting_period = nil
82
+
83
+ @report_cache.should_not be_valid
84
+ end
85
+
86
+ it 'should not succeed when a blank reporting_period is set' do
87
+ @report_cache.reporting_period = ''
88
+
89
+ @report_cache.should_not be_valid
90
+ end
91
+
92
+ end
93
+
9
94
  describe '.clear_for' do
10
95
 
11
96
  it 'should delete all entries in the cache for the klass and report name' do