compendium 1.1.3.4 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +5 -13
  2. data/.travis.yml +12 -0
  3. data/CHANGELOG.md +18 -0
  4. data/Gemfile +4 -0
  5. data/README.md +185 -4
  6. data/app/assets/stylesheets/compendium/options.css.scss +2 -3
  7. data/app/classes/compendium/presenters/chart.rb +1 -1
  8. data/app/classes/compendium/presenters/csv.rb +32 -0
  9. data/app/classes/compendium/presenters/query.rb +4 -2
  10. data/app/classes/compendium/presenters/settings/query.rb +10 -2
  11. data/app/classes/compendium/presenters/settings/table.rb +24 -4
  12. data/app/classes/compendium/presenters/table.rb +33 -18
  13. data/app/controllers/compendium/reports_controller.rb +28 -7
  14. data/app/views/compendium/reports/setup.haml +16 -2
  15. data/compendium.gemspec +4 -3
  16. data/config/locales/en.yml +6 -1
  17. data/config/locales/es.yml +11 -0
  18. data/config/locales/fr.yml +11 -0
  19. data/lib/compendium.rb +1 -0
  20. data/lib/compendium/collection_query.rb +1 -1
  21. data/lib/compendium/count_query.rb +7 -1
  22. data/lib/compendium/dsl.rb +45 -4
  23. data/lib/compendium/engine/mount.rb +13 -5
  24. data/lib/compendium/errors.rb +6 -0
  25. data/lib/compendium/metric.rb +1 -1
  26. data/lib/compendium/open_hash.rb +1 -1
  27. data/lib/compendium/param_types.rb +2 -2
  28. data/lib/compendium/params.rb +1 -1
  29. data/lib/compendium/query.rb +23 -4
  30. data/lib/compendium/report.rb +14 -10
  31. data/lib/compendium/sum_query.rb +3 -1
  32. data/lib/compendium/through_query.rb +7 -2
  33. data/lib/compendium/version.rb +1 -1
  34. data/spec/count_query_spec.rb +41 -5
  35. data/spec/dsl_spec.rb +77 -2
  36. data/spec/presenters/csv_spec.rb +30 -0
  37. data/spec/presenters/settings/query_spec.rb +26 -0
  38. data/spec/presenters/settings/table_spec.rb +64 -0
  39. data/spec/presenters/table_spec.rb +83 -0
  40. data/spec/query_spec.rb +55 -8
  41. data/spec/report_spec.rb +24 -1
  42. data/spec/sum_query_spec.rb +40 -5
  43. metadata +73 -30
  44. data/config/initializers/ruby/hash.rb +0 -6
@@ -5,8 +5,8 @@ module Compendium
5
5
 
6
6
  before_filter :find_report
7
7
  before_filter :find_query
8
- before_filter :validate_options, only: :run
9
- before_filter :run_report, only: :run
8
+ before_filter :validate_options, only: [:run, :export]
9
+ before_filter :run_report, only: [:run, :export]
10
10
 
11
11
  def setup
12
12
  render_setup
@@ -25,6 +25,27 @@ module Compendium
25
25
  end
26
26
  end
27
27
 
28
+ def export
29
+ unless @report.exports?(request.format)
30
+ redirect_to action: :setup, format: nil
31
+ return
32
+ end
33
+
34
+ respond_to do |format|
35
+ format.csv do
36
+ filename = @report.report_name.to_s.parameterize + '-' + Time.current.strftime('%Y%m%d%H%I%S')
37
+ response.headers['Content-Disposition'] = 'attachment; filename="' + filename + '.csv"'
38
+
39
+ query = @report.queries[@report.exporters[:csv]]
40
+ render text: query.render_csv
41
+ end
42
+
43
+ format.any do
44
+ redirect_to action: :setup, format: nil
45
+ end
46
+ end
47
+ end
48
+
28
49
  private
29
50
 
30
51
  def find_report
@@ -32,11 +53,11 @@ module Compendium
32
53
  @report_name = "#{@prefix}_report"
33
54
 
34
55
  begin
35
- require(@report_name) unless Rails.env.development? or Module.const_defined?(@report_name.classify)
56
+ require(@report_name) unless Rails.env.development? || Module.const_defined?(@report_name.classify)
36
57
  @report_class = @report_name.camelize.constantize
37
58
  @report = setup_report
38
59
  rescue LoadError
39
- flash[:error] = t(:invalid_report)
60
+ flash[:error] = t(:invalid_report, scope: 'compendium.reports')
40
61
  redirect_to action: :index
41
62
  end
42
63
  end
@@ -46,7 +67,7 @@ module Compendium
46
67
  @query = @report.queries[params[:query]]
47
68
 
48
69
  unless @query
49
- flash[:error] = t(:invalid_report_query)
70
+ flash[:error] = t(:invalid_report_query, scope: 'compendium.reports')
50
71
  redirect_to action: :setup, report_name: params[:report_name]
51
72
  end
52
73
  end
@@ -61,7 +82,7 @@ module Compendium
61
82
  end
62
83
 
63
84
  def validate_options
64
- render_setup and return unless @report.valid?
85
+ render_setup && return unless @report.valid?
65
86
  end
66
87
 
67
88
  def run_report
@@ -80,4 +101,4 @@ module Compendium
80
101
  paths
81
102
  end
82
103
  end
83
- end
104
+ end
@@ -4,11 +4,25 @@
4
4
  = render_if_exists partial: 'report_header', path: 'compendium/reports'
5
5
 
6
6
  .options
7
- = form_for report, as: :report, url: compendium_reports_run_path do |f|
7
+ = form_for report, as: :report, url: compendium_reports_run_path, html: { id: 'setup_report_form' } do |f|
8
+ = hidden_field_tag :format, :html
9
+
8
10
  - report.options.each do |option|
9
11
  - expose option, Compendium::Presenters::Option do |opt|
10
12
  .option
11
13
  = opt.label(f)
12
14
  .option-element-group= opt.input(self, f)
13
15
 
14
- = f.submit t(:generate_report, scope: 'compendium.reports')
16
+ .option
17
+ = f.submit t(:generate_report, scope: 'compendium.reports'), onclick: "set_format('html')"
18
+ - if report.exports?(:csv)
19
+ = f.submit t(:export_csv, scope: 'compendium.reports'), name: :export, onclick: "set_format('csv')"
20
+
21
+ :javascript
22
+ function set_format(format)
23
+ {
24
+ var form = document.getElementById('setup_report_form'),
25
+ input = form.querySelector('input#format[type=hidden]');
26
+
27
+ input.value = format;
28
+ }
data/compendium.gemspec CHANGED
@@ -18,10 +18,11 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.add_dependency 'rails', '>= 3.0.0'
21
+ gem.add_dependency 'rails', '>= 3.0.0', '< 4'
22
22
  gem.add_dependency 'sass-rails', '>= 3.0.0'
23
23
  gem.add_dependency 'compass-rails', '>= 1.0.0'
24
- gem.add_dependency 'collection_of', '1.0.5'
24
+ gem.add_dependency 'collection_of', '1.0.6'
25
25
  gem.add_dependency 'inheritable_attr', '>= 1.0.0'
26
- gem.add_development_dependency 'rspec', '~> 2.0'
26
+ gem.add_development_dependency 'rake', '> 11.0.1', '< 12'
27
+ gem.add_development_dependency 'rspec', '~> 2.0', '< 2.99'
27
28
  end
@@ -1,6 +1,11 @@
1
1
  en:
2
2
  compendium:
3
+ total: "Total"
4
+
3
5
  reports:
6
+ generate_report: "Generate Report"
7
+ export_csv: "Export CSV"
8
+
4
9
  invalid_report: "Invalid report!"
5
10
  invalid_report_query: "Invalid query!"
6
- generate_report: "Generate Report"
11
+ query_required_for_csv: "A query must be specified to export a CSV file!"
@@ -0,0 +1,11 @@
1
+ es:
2
+ compendium:
3
+ total: "Total"
4
+
5
+ reports:
6
+ generate_report: "Generar informe"
7
+ export_csv: "Exportar CSV"
8
+
9
+ invalid_report: "¡Informe no válido!"
10
+ invalid_report_query: "¡Consulta no válida!"
11
+ query_required_for_csv: "¡Se debe especificar una consulta para exportar un archivo CSV!"
@@ -0,0 +1,11 @@
1
+ fr:
2
+ compendium:
3
+ total: "Total"
4
+
5
+ reports:
6
+ generate_report: "Générer Rapport"
7
+ export_csv: "Exporter CSV"
8
+
9
+ invalid_report: "Rapport invalide!"
10
+ invalid_report_query: "Requête invalide!"
11
+ query_required_for_csv: "Une requête doit être spécifiée afin d'exporter un fichier CSV!"
data/lib/compendium.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'compendium/engine'
2
+ require 'compendium/errors'
2
3
  require 'compendium/version'
3
4
  require 'active_support/configurable'
4
5
 
@@ -37,7 +37,7 @@ module Compendium
37
37
  end
38
38
 
39
39
  def prepare_collection(collection)
40
- return collection if collection.is_a?(Query) or collection.is_a?(Symbol)
40
+ return collection if collection.is_a?(Query) || collection.is_a?(Symbol)
41
41
  collection.is_a?(Hash) ? collection : Hash[collection.zip(collection)]
42
42
  end
43
43
  end
@@ -1,10 +1,16 @@
1
+ require 'compendium/errors'
1
2
  require 'compendium/query'
2
3
 
3
4
  module Compendium
4
5
  # A CountQuery is a Query which runs an SQL count statement
5
6
  # Often useful in conjunction with a grouped query
6
7
  class CountQuery < Query
7
- InvalidCommand = Class.new(StandardError)
8
+
9
+ def initialize(*args)
10
+ super
11
+
12
+ @options.reverse_merge!(order: 'COUNT(*)', reverse: true)
13
+ end
8
14
 
9
15
  private
10
16
 
@@ -8,14 +8,17 @@ module Compendium
8
8
  def self.extended(klass)
9
9
  klass.inheritable_attr :queries, default: ::Collection[Query]
10
10
  klass.inheritable_attr :options, default: ::Collection[Option]
11
+ klass.inheritable_attr :exporters, default: {}
11
12
  end
12
13
 
14
+ # Define a query
13
15
  def query(name, opts = {}, &block)
14
16
  define_query(name, opts, &block)
15
17
  end
16
18
  alias_method :chart, :query
17
19
  alias_method :data, :query
18
20
 
21
+ # Define a parameter for the report
19
22
  def option(name, *args)
20
23
  opts = args.extract_options!
21
24
  type = args.shift
@@ -31,6 +34,8 @@ module Compendium
31
34
  end
32
35
  end
33
36
 
37
+ # Define a metric from a query or implicitly
38
+ # A metric is a derived statistic from a report, for instance a count of rows
34
39
  def metric(name, *args, &block)
35
40
  proc = args.first.is_a?(Proc) ? args.first : block
36
41
  opts = args.extract_options!
@@ -49,10 +54,32 @@ module Compendium
49
54
  end
50
55
  end
51
56
 
57
+ # Define a filter to modify the results from specified query (in this case :deliveries)
58
+ # For example, this can be useful to translate columns prior to rendering, as it will apply
59
+ # for all render types (table, chart, JSON)
60
+ # Multiple queries can be set up with the same filter
52
61
  def filter(*query_names, &block)
53
- query_names.each do |query_name|
54
- raise ArgumentError, "query #{query_name} is not defined" unless queries.key?(query_name)
55
- queries[query_name].add_filter(block)
62
+ each_query(query_names) do |query|
63
+ query.add_filter(block)
64
+ end
65
+ end
66
+
67
+ # Allow default table settings to be defined for a query.
68
+ # These settings are used when rendering a query to an HTML table or to CSV
69
+ def table(*query_names, &block)
70
+ each_query(query_names) do |query|
71
+ query.table_settings = block
72
+ end
73
+ end
74
+
75
+ # Define any exports the report has
76
+ def exports(type, *opts)
77
+ exporters[type] = if opts.empty?
78
+ true
79
+ elsif opts.length == 1
80
+ opts.first
81
+ else
82
+ opts
56
83
  end
57
84
  end
58
85
 
@@ -86,6 +113,13 @@ module Compendium
86
113
 
87
114
  private
88
115
 
116
+ def each_query(query_names, &block)
117
+ query_names.each do |query_name|
118
+ raise ArgumentError, "query #{query_name} is not defined" unless queries.key?(query_name)
119
+ yield queries[query_name]
120
+ end
121
+ end
122
+
89
123
  def define_query(name, opts, &block)
90
124
  params = [name.to_sym, opts, block]
91
125
  query_type = Query
@@ -110,7 +144,14 @@ module Compendium
110
144
  query.report = self
111
145
 
112
146
  metrics[name] = opts[:metric] if opts.key?(:metric)
147
+
148
+ if queries[name]
149
+ raise CannotRedefineQueryType unless queries[name].instance_of?(query_type)
150
+ queries.delete(name)
151
+ end
152
+
113
153
  queries << query
154
+
114
155
  query
115
156
  end
116
157
 
@@ -119,4 +160,4 @@ module Compendium
119
160
  self.params_class.validates name, validations
120
161
  end
121
162
  end
122
- end
163
+ end
@@ -1,13 +1,21 @@
1
1
  module ActionDispatch
2
2
  module Routing
3
3
  class Mapper
4
+ class ExportRouter
5
+ def matches?(request)
6
+ request.params[:export].present?
7
+ end
8
+ end
9
+
4
10
  def mount_compendium(options = {})
5
- scope options[:at], controller: options.fetch(:controller, 'compendium/reports') do
6
- get ':report_name', action: :setup, as: 'compendium_reports_setup'
7
- post ':report_name(/:query)', action: :run, as: 'compendium_reports_run'
8
- root action: :index, as: 'compendium_reports_root'
11
+ scope options[:at], controller: options.fetch(:controller, 'compendium/reports'), as: 'compendium_reports' do
12
+ get ':report_name', action: :setup, constraints: { format: :html }, as: 'setup'
13
+ match ':report_name/export', action: :export, as: 'export', via: [:get, :post]
14
+ post ':report_name(/:query)', constraints: ExportRouter.new, action: :export, as: 'export_post'
15
+ match ':report_name(/:query)', action: :run, as: 'run', via: [:get, :post]
16
+ root action: :index, as: 'root'
9
17
  end
10
18
  end
11
19
  end
12
20
  end
13
- end
21
+ end
@@ -0,0 +1,6 @@
1
+ module Compendium
2
+ CompendiumError = Class.new(StandardError)
3
+
4
+ InvalidCommand = Class.new(CompendiumError)
5
+ CannotRedefineQueryType = Class.new(CompendiumError)
6
+ end
@@ -27,7 +27,7 @@ module Compendium
27
27
  private
28
28
 
29
29
  def condition_failed?(ctx)
30
- (options.key?(:if) and !ctx.instance_exec(&options[:if])) or (options.key?(:unless) and ctx.instance_exec(&options[:unless]))
30
+ (options.key?(:if) && !ctx.instance_exec(&options[:if])) || (options.key?(:unless) && ctx.instance_exec(&options[:unless]))
31
31
  end
32
32
  end
33
33
  end
@@ -45,7 +45,7 @@ module Compendium
45
45
  return self[method[1..-1]] if self.key?(method[1..-1].to_sym)
46
46
 
47
47
  else
48
- return self[method] if key?(method) or !respond_to?(method)
48
+ return self[method] if key?(method) || !respond_to?(method)
49
49
  end
50
50
 
51
51
  super
@@ -38,7 +38,7 @@ module Compendium
38
38
  index = obj
39
39
  else
40
40
  index = obj.numeric? ? obj.to_i : @choices.index(obj)
41
- raise IndexError if (!obj.nil? and index.nil?) or index.to_i.abs > @choices.length - 1
41
+ raise IndexError if (!obj.nil? && index.nil?) || index.to_i.abs > @choices.length - 1
42
42
  end
43
43
 
44
44
  super(index)
@@ -63,7 +63,7 @@ module Compendium
63
63
  class BooleanParam < Param
64
64
  def initialize(obj, *)
65
65
  # If given 0, 1, or a version thereof (ie. "0"), pass it along
66
- return super obj.to_i if obj.numeric? and (0..1).cover?(obj.to_i)
66
+ return super obj.to_i if obj.numeric? && (0..1).cover?(obj.to_i)
67
67
  super !!obj ? 0 : 1
68
68
  end
69
69
 
@@ -37,7 +37,7 @@ module Compendium
37
37
  end
38
38
 
39
39
  def get_default_value(current, default)
40
- if current.blank? and !default.blank?
40
+ if current.blank? && !default.blank?
41
41
  default.respond_to?(:call) ? default.call : default
42
42
  else
43
43
  current
@@ -4,12 +4,11 @@ require 'compendium/metric'
4
4
  require 'compendium/presenters/chart'
5
5
  require 'compendium/presenters/table'
6
6
  require 'collection_of'
7
- require_relative '../../config/initializers/ruby/hash'
8
7
 
9
8
  module Compendium
10
9
  class Query
11
10
  attr_reader :name, :results, :metrics, :filters
12
- attr_accessor :options, :proc, :report
11
+ attr_accessor :options, :proc, :report, :table_settings
13
12
 
14
13
  def initialize(*args)
15
14
  @report = args.shift if arg_is_report?(args.first)
@@ -58,6 +57,10 @@ module Compendium
58
57
  Compendium::Presenters::Table.new(template, self, *options, &block).render unless empty?
59
58
  end
60
59
 
60
+ def render_csv(&block)
61
+ Compendium::Presenters::CSV.new(self, &block).render unless empty?
62
+ end
63
+
61
64
  # Allow access to the chart object without having to explicitly render it
62
65
  def chart(template, *options, &block)
63
66
  # Access the actual chart object
@@ -89,6 +92,8 @@ module Compendium
89
92
 
90
93
  def collect_results(context, *params)
91
94
  command = context.instance_exec(*params, &proc) if proc
95
+ command = order_command(command) if options[:order]
96
+
92
97
  results = fetch_results(command)
93
98
  results = filter_results(results, *params) if filters.any?
94
99
  @results = ResultSet.new(results) if results
@@ -105,6 +110,12 @@ module Compendium
105
110
  def filter_results(results, params)
106
111
  return unless results
107
112
 
113
+ if results.respond_to? :with_indifferent_access
114
+ results = results.with_indifferent_access
115
+ else
116
+ results.map! &:with_indifferent_access
117
+ end
118
+
108
119
  filters.each do |f|
109
120
  if f.arity == 2
110
121
  results = f.call(results, params)
@@ -116,6 +127,14 @@ module Compendium
116
127
  results
117
128
  end
118
129
 
130
+ def order_command(command)
131
+ return command unless command.respond_to?(:order)
132
+
133
+ command = command.order(options[:order])
134
+ command = command.reverse_order if options.fetch(:reverse, false)
135
+ command
136
+ end
137
+
119
138
  def execute_command(command)
120
139
  return [] if command.nil?
121
140
  command = command.to_sql if command.respond_to?(:to_sql)
@@ -127,11 +146,11 @@ module Compendium
127
146
  end
128
147
 
129
148
  def arg_is_report?(arg)
130
- arg.is_a?(Report) or (arg.is_a?(Class) and arg < Report)
149
+ arg.is_a?(Report) || (arg.is_a?(Class) && arg < Report)
131
150
  end
132
151
 
133
152
  def get_associated_query(query)
134
153
  query.is_a?(Query) ? query : report.queries[query]
135
154
  end
136
155
  end
137
- end
156
+ end