noventius 1.0.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +22 -0
- data/app/assets/javascripts/nuntius/application.js +39 -0
- data/app/assets/javascripts/nuntius/columns.js +57 -0
- data/app/assets/javascripts/nuntius/filters/select.js +88 -0
- data/app/assets/javascripts/nuntius/filters.js +44 -0
- data/app/assets/javascripts/nuntius/forms.js +38 -0
- data/app/assets/javascripts/nuntius/nested.js +25 -0
- data/app/assets/javascripts/nuntius/reports.js +7 -0
- data/app/assets/stylesheets/nuntius/application.css +36 -0
- data/app/assets/stylesheets/nuntius/nested.css +8 -0
- data/app/assets/stylesheets/nuntius/reports.css +17 -0
- data/app/controllers/concerns/nuntius/filter_params.rb +23 -0
- data/app/controllers/nuntius/application_controller.rb +17 -0
- data/app/controllers/nuntius/reports_controller.rb +45 -0
- data/app/helpers/concerns/nuntius/filter_wrappers.rb +86 -0
- data/app/helpers/nuntius/alerts_helper.rb +48 -0
- data/app/helpers/nuntius/application_helper.rb +19 -0
- data/app/helpers/nuntius/cells_helper.rb +24 -0
- data/app/helpers/nuntius/columns_helper.rb +47 -0
- data/app/helpers/nuntius/filters_helper.rb +147 -0
- data/app/helpers/nuntius/forms_helper.rb +18 -0
- data/app/helpers/nuntius/rows_helper.rb +21 -0
- data/app/views/layouts/nuntius/application.html.erb +21 -0
- data/app/views/nuntius/reports/_filter.erb +10 -0
- data/app/views/nuntius/reports/_form.erb +26 -0
- data/app/views/nuntius/reports/_table.erb +21 -0
- data/app/views/nuntius/reports/index.erb +0 -0
- data/app/views/nuntius/reports/nested.erb +1 -0
- data/app/views/nuntius/reports/show.erb +8 -0
- data/app/views/shared/nuntius/_header.erb +16 -0
- data/config/initializers/assets.rb +1 -0
- data/config/routes.rb +5 -0
- data/lib/nuntius/column.rb +64 -0
- data/lib/nuntius/columns_group.rb +38 -0
- data/lib/nuntius/engine.rb +15 -0
- data/lib/nuntius/extensions/date_query.rb +62 -0
- data/lib/nuntius/filter.rb +46 -0
- data/lib/nuntius/post_processors/date_ranges.rb +120 -0
- data/lib/nuntius/report/dsl/columns.rb +132 -0
- data/lib/nuntius/report/dsl/filters.rb +50 -0
- data/lib/nuntius/report/dsl/nested.rb +66 -0
- data/lib/nuntius/report/dsl/post_processors.rb +40 -0
- data/lib/nuntius/report/dsl/validations.rb +40 -0
- data/lib/nuntius/report/dsl.rb +42 -0
- data/lib/nuntius/report/interpolator.rb +75 -0
- data/lib/nuntius/report.rb +81 -0
- data/lib/nuntius/serializers/csv.rb +54 -0
- data/lib/nuntius/validation.rb +30 -0
- data/lib/nuntius/version.rb +5 -0
- data/lib/nuntius.rb +13 -0
- data/lib/tasks/nuntius_tasks.rake +4 -0
- metadata +251 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
module Nuntius
|
2
|
+
|
3
|
+
module PostProcessors
|
4
|
+
|
5
|
+
class DateRanges
|
6
|
+
|
7
|
+
DATE_STEPS = %i(day month)
|
8
|
+
STEPS = %i(day month hour dow moy)
|
9
|
+
|
10
|
+
def initialize(column_index_or_name, step, time_zone = 'America/Montevideo')
|
11
|
+
fail ArgumentError, "Step not supported [#{step}]." unless STEPS.include?(step.to_sym)
|
12
|
+
|
13
|
+
@column_index_or_name = column_index_or_name
|
14
|
+
@step = step.to_sym
|
15
|
+
@time_zone = time_zone
|
16
|
+
end
|
17
|
+
|
18
|
+
def process(report, rows)
|
19
|
+
return [] if rows.empty?
|
20
|
+
|
21
|
+
rows_by_date = group_rows_by_date(report, rows)
|
22
|
+
|
23
|
+
start_date = rows_by_date.keys.min
|
24
|
+
end_date = rows_by_date.keys.max
|
25
|
+
|
26
|
+
empty_row = build_empty_row(report)
|
27
|
+
|
28
|
+
build_range(start_date, end_date).map do |value|
|
29
|
+
rows_by_date.fetch(value, [value].concat(empty_row))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def group_rows_by_date(report, rows)
|
36
|
+
column_index = get_column_index(report, rows.first.is_a?(Hash))
|
37
|
+
|
38
|
+
rows.inject({}) do |result, row|
|
39
|
+
row[column_index] = parse_date_column(row[column_index])
|
40
|
+
result.merge(row[column_index].to_i => row)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_date_column(value)
|
45
|
+
if DATE_STEPS.include?(@step)
|
46
|
+
Time.parse(value).in_time_zone(@time_zone)
|
47
|
+
else
|
48
|
+
value.to_i
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_column_index(report, hash_rows)
|
53
|
+
if hash_rows
|
54
|
+
@column_index_or_name.to_s
|
55
|
+
elsif @column_index_or_name.is_a?(Integer)
|
56
|
+
@column_index_or_name
|
57
|
+
else
|
58
|
+
report.column_index(@column_index_or_name)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def build_empty_row(report)
|
63
|
+
columns_count = report.columns.size
|
64
|
+
[''] * (columns_count - 1)
|
65
|
+
end
|
66
|
+
|
67
|
+
def build_range(start_value, end_value)
|
68
|
+
case @step
|
69
|
+
when :day
|
70
|
+
(DayRange.new(start_value)..DayRange.new(end_value)).map(&:date)
|
71
|
+
when :month
|
72
|
+
(MonthRange.new(start_value)..MonthRange.new(end_value)).map(&:date)
|
73
|
+
else
|
74
|
+
start_value..end_value
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class BaseRange
|
79
|
+
|
80
|
+
include Comparable
|
81
|
+
|
82
|
+
attr_reader :date
|
83
|
+
|
84
|
+
def initialize(date)
|
85
|
+
@date = date
|
86
|
+
end
|
87
|
+
|
88
|
+
def succ
|
89
|
+
self.class.new(@date + @step)
|
90
|
+
end
|
91
|
+
|
92
|
+
def <=>(other)
|
93
|
+
@date <=> other.date
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
class DayRange < BaseRange
|
99
|
+
|
100
|
+
def initialize(date)
|
101
|
+
super
|
102
|
+
@step = 1.day
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
class MonthRange < BaseRange
|
108
|
+
|
109
|
+
def initialize(date)
|
110
|
+
super
|
111
|
+
@step = 1.month
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Nuntius
|
2
|
+
|
3
|
+
class Report
|
4
|
+
|
5
|
+
module Dsl
|
6
|
+
|
7
|
+
module Columns
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.extend ClassMethods
|
11
|
+
base.send :include, InstanceMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
def columns
|
17
|
+
@columns ||= []
|
18
|
+
end
|
19
|
+
|
20
|
+
def columns_names
|
21
|
+
@columns_names ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
def column(name, type, options = {})
|
25
|
+
validate_name_not_taken!(name)
|
26
|
+
|
27
|
+
Column.new(name, type, options).tap do |column|
|
28
|
+
columns_names << column.name
|
29
|
+
columns << column
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def columns_group(name, children, options = {})
|
34
|
+
validate_name_not_taken!(name)
|
35
|
+
|
36
|
+
ColumnsGroup.new(name, children, options).tap do |columns_group|
|
37
|
+
columns_names << columns_group.name
|
38
|
+
children.map { |child| columns.delete(child) }
|
39
|
+
|
40
|
+
columns << columns_group
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def dynamic_columns(method_name)
|
45
|
+
columns << -> { send(method_name) }
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def validate_name_not_taken!(name)
|
51
|
+
fail "Column name: #{name} is taken" if columns_names.include?(name.to_sym)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
module InstanceMethods
|
57
|
+
|
58
|
+
def columns
|
59
|
+
return @columns if @columns
|
60
|
+
|
61
|
+
build_columns
|
62
|
+
|
63
|
+
@columns
|
64
|
+
end
|
65
|
+
|
66
|
+
def columns_without_groups
|
67
|
+
return @columns_without_groups if @columns_without_groups
|
68
|
+
|
69
|
+
build_columns
|
70
|
+
|
71
|
+
@columns_without_groups
|
72
|
+
end
|
73
|
+
|
74
|
+
def columns_names
|
75
|
+
@columns_names ||= self.class.columns_names.dup
|
76
|
+
end
|
77
|
+
|
78
|
+
def column(name, type, options = {})
|
79
|
+
validate_name_not_taken!(name)
|
80
|
+
|
81
|
+
Column.new(name, type, options).tap do |column|
|
82
|
+
columns_names << column.name
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def column_group(name, children, options = {})
|
87
|
+
validate_name_not_taken!(name)
|
88
|
+
|
89
|
+
ColumnsGroup.new(name, children, options).tap do |columns_group|
|
90
|
+
columns_names << columns_group.name
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def column_index(name)
|
95
|
+
columns_without_groups.find_index { |column| column.name == name }
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def validate_name_not_taken!(name)
|
101
|
+
fail "Column name: #{name} is taken" if columns_names.include?(name.to_sym)
|
102
|
+
end
|
103
|
+
|
104
|
+
def build_columns
|
105
|
+
@columns = self.class.columns.map do |column|
|
106
|
+
column = instance_exec(&column) if column.is_a?(Proc)
|
107
|
+
|
108
|
+
column
|
109
|
+
end.flatten
|
110
|
+
|
111
|
+
@columns_without_groups = build_columns_without_groups(@columns)
|
112
|
+
end
|
113
|
+
|
114
|
+
def build_columns_without_groups(columns)
|
115
|
+
columns.map do |column|
|
116
|
+
if column.is_a?(Nuntius::Column)
|
117
|
+
column
|
118
|
+
else
|
119
|
+
build_columns_without_groups(column.columns)
|
120
|
+
end
|
121
|
+
end.flatten
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Nuntius
|
2
|
+
|
3
|
+
class Report
|
4
|
+
|
5
|
+
module Dsl
|
6
|
+
|
7
|
+
module Filters
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.extend ClassMethods
|
11
|
+
base.send :include, InstanceMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
def filters
|
17
|
+
(@filters ||= []).sort_by! { |f| f.options[:priority] }
|
18
|
+
end
|
19
|
+
|
20
|
+
def filter(name, type, args = {})
|
21
|
+
filters << Filter.new(name, type, args)
|
22
|
+
define_filter_accessors(name, type)
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def define_filter_accessors(name, _type)
|
28
|
+
define_method(name) { filter_params[name] }
|
29
|
+
define_method("#{name}=") { |value| filter_params[name] = value }
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
module InstanceMethods
|
35
|
+
|
36
|
+
attr_reader :filter_params
|
37
|
+
|
38
|
+
def filters
|
39
|
+
self.class.filters.deep_dup
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Nuntius
|
2
|
+
|
3
|
+
class Report
|
4
|
+
|
5
|
+
module Dsl
|
6
|
+
|
7
|
+
module Nested
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.extend ClassMethods
|
11
|
+
base.send :include, InstanceMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
attr_reader :nested_options
|
17
|
+
attr_reader :nested_report_class
|
18
|
+
|
19
|
+
def nest_report(klass, options = {})
|
20
|
+
@nested_report_class = klass
|
21
|
+
@nested_options = options
|
22
|
+
end
|
23
|
+
|
24
|
+
def includes_nested?
|
25
|
+
@nested_report_class != nil
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
module InstanceMethods
|
31
|
+
|
32
|
+
def nested_options
|
33
|
+
self.class.nested_options
|
34
|
+
end
|
35
|
+
|
36
|
+
def enable_nested?
|
37
|
+
return @enable_nested unless @enable_nested.nil?
|
38
|
+
|
39
|
+
return false unless self.class.includes_nested?
|
40
|
+
|
41
|
+
@enable_nested = nested_options.fetch(:if, true)
|
42
|
+
@enable_nested = public_send(@enable_nested) if @enable_nested.is_a?(Symbol)
|
43
|
+
@enable_nested = instance_exec(&@enable_nested) if @enable_nested.is_a?(Proc)
|
44
|
+
|
45
|
+
@enable_nested
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_nested_report(row)
|
49
|
+
row ||= []
|
50
|
+
|
51
|
+
nested_filters = nested_options[:filters] || filter_params
|
52
|
+
nested_filters = public_send(nested_filters, row) if nested_filters.is_a?(Symbol)
|
53
|
+
nested_filters = instance_exec(row, &nested_filters) if nested_filters.is_a?(Proc)
|
54
|
+
|
55
|
+
self.class.nested_report_class.new(nested_filters)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Nuntius
|
2
|
+
|
3
|
+
class Report
|
4
|
+
|
5
|
+
module Dsl
|
6
|
+
|
7
|
+
module PostProcessors
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.extend ClassMethods
|
11
|
+
base.send :include, InstanceMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
def post_processors
|
17
|
+
@post_processors ||= []
|
18
|
+
end
|
19
|
+
|
20
|
+
def post_processor(post_processor, options = {})
|
21
|
+
post_processors << [post_processor, options]
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
module InstanceMethods
|
27
|
+
|
28
|
+
def post_processors
|
29
|
+
self.class.post_processors
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Nuntius
|
2
|
+
|
3
|
+
class Report
|
4
|
+
|
5
|
+
module Dsl
|
6
|
+
|
7
|
+
module Validations
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.extend ClassMethods
|
11
|
+
base.send :include, InstanceMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
def validations
|
17
|
+
@validations ||= []
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate(name, rules: {}, messages: {})
|
21
|
+
validations << Validation.new(name, rules, messages)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
module InstanceMethods
|
27
|
+
|
28
|
+
def validations
|
29
|
+
self.class.validations.deep_dup
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative 'dsl/filters'
|
2
|
+
require_relative 'dsl/columns'
|
3
|
+
require_relative 'dsl/nested'
|
4
|
+
require_relative 'dsl/post_processors'
|
5
|
+
require_relative 'dsl/validations'
|
6
|
+
|
7
|
+
module Nuntius
|
8
|
+
|
9
|
+
class Report
|
10
|
+
|
11
|
+
module Dsl
|
12
|
+
|
13
|
+
def self.included(base)
|
14
|
+
base.extend ClassMethods
|
15
|
+
base.send :include, InstanceMethods
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
|
20
|
+
include Filters::ClassMethods
|
21
|
+
include Columns::ClassMethods
|
22
|
+
include Nested::ClassMethods
|
23
|
+
include PostProcessors::ClassMethods
|
24
|
+
include Validations::ClassMethods
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
module InstanceMethods
|
29
|
+
|
30
|
+
include Filters::InstanceMethods
|
31
|
+
include Columns::InstanceMethods
|
32
|
+
include Nested::InstanceMethods
|
33
|
+
include PostProcessors::InstanceMethods
|
34
|
+
include Validations::InstanceMethods
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Nuntius
|
2
|
+
|
3
|
+
class Report
|
4
|
+
|
5
|
+
module Interpolator
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend ClassMethods
|
9
|
+
base.send :include, InstanceMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
module InstanceMethods
|
17
|
+
|
18
|
+
UNESCAPED_REGEX = /(<%\w+%>)+/
|
19
|
+
UNESCAPED_VARIABLE_REGEX = /(?:<%)(\w+)(?:%>)/
|
20
|
+
|
21
|
+
ESCAPED_REGEX = /({\w+})+/
|
22
|
+
ESCAPED_VARIABLE_REGEX = /(?:{)(\w+)(?:})/
|
23
|
+
|
24
|
+
# Interpolate the given text in the context of `self`
|
25
|
+
#
|
26
|
+
# Interpolated variables will try to call a method on `self` for obtaining the value.
|
27
|
+
#
|
28
|
+
# Two syntax are supported at the moment.
|
29
|
+
#
|
30
|
+
# Let's say we have a method called foo that return String 'bar'
|
31
|
+
#
|
32
|
+
# {foo} => 'bar'
|
33
|
+
# <%foo%> => bar
|
34
|
+
#
|
35
|
+
# @param [String] text The text to interpolate
|
36
|
+
# @return [String] The interpolated text
|
37
|
+
def interpolate(text)
|
38
|
+
return unless text
|
39
|
+
|
40
|
+
# Interpolate escaped variables
|
41
|
+
text.scan(ESCAPED_REGEX).flatten.each do |var|
|
42
|
+
var_name = var.match(ESCAPED_VARIABLE_REGEX)[1]
|
43
|
+
value = send(var_name.to_sym)
|
44
|
+
|
45
|
+
text.gsub!(var, escape(value))
|
46
|
+
end
|
47
|
+
|
48
|
+
# Interpolate unescaped variables
|
49
|
+
text.scan(UNESCAPED_REGEX).flatten.each do |var|
|
50
|
+
var_name = var.match(UNESCAPED_VARIABLE_REGEX)[1]
|
51
|
+
value = send(var_name.to_sym)
|
52
|
+
|
53
|
+
text.gsub!(var, value)
|
54
|
+
end
|
55
|
+
|
56
|
+
text
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def escape(value)
|
62
|
+
if value.respond_to?(:map)
|
63
|
+
"(#{value.map { |v| escape(v) }.join(', ')})"
|
64
|
+
else
|
65
|
+
ActiveRecord::Base.connection.quote(value)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require_relative 'report/dsl'
|
2
|
+
require_relative 'report/interpolator'
|
3
|
+
require_relative 'serializers/csv'
|
4
|
+
|
5
|
+
module Nuntius
|
6
|
+
|
7
|
+
class Report
|
8
|
+
|
9
|
+
include Dsl
|
10
|
+
include Interpolator
|
11
|
+
|
12
|
+
class << self
|
13
|
+
|
14
|
+
attr_reader :tab_title
|
15
|
+
|
16
|
+
def title(title)
|
17
|
+
@tab_title = title
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(filter_params = {})
|
23
|
+
@filter_params = filter_params
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.all
|
27
|
+
Dir.glob(File.expand_path('app/reports/*.rb', Rails.root)).map do |file|
|
28
|
+
file[%r{app\/reports\/(.*)\.rb}, 1].classify.constantize
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def result
|
33
|
+
@result ||= ActiveRecord::Base.connection.exec_query(interpolate(sql))
|
34
|
+
end
|
35
|
+
|
36
|
+
def columns
|
37
|
+
columns = super
|
38
|
+
|
39
|
+
columns = result.columns.map { |column| Column.new(column, :string) } unless columns.any?
|
40
|
+
|
41
|
+
columns
|
42
|
+
end
|
43
|
+
|
44
|
+
def rows # rubocop:disable Rails/Delegate
|
45
|
+
result.rows
|
46
|
+
end
|
47
|
+
|
48
|
+
def processed_rows
|
49
|
+
post_processors.inject(rows) do |rows, (post_processor, options)|
|
50
|
+
execute = options.fetch(:if, true)
|
51
|
+
execute = instance_exec(&execute) if execute.is_a?(Proc)
|
52
|
+
execute = public_send(execute) if execute.is_a?(Symbol)
|
53
|
+
|
54
|
+
return rows unless execute
|
55
|
+
|
56
|
+
if post_processor.is_a?(Proc)
|
57
|
+
instance_exec(rows, &post_processor)
|
58
|
+
elsif post_processor.is_a?(Symbol)
|
59
|
+
public_send(post_processor, rows)
|
60
|
+
else
|
61
|
+
post_processor.process(self, rows)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def to(format)
|
67
|
+
case format
|
68
|
+
when :csv
|
69
|
+
Serializers::Csv.new(self).generate
|
70
|
+
else
|
71
|
+
fail NotImplementedError, "No serializer found for: #{format}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def sql
|
76
|
+
fail NotImplementedError, "Abstract method #{__method__}"
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module Nuntius
|
4
|
+
|
5
|
+
module Serializers
|
6
|
+
|
7
|
+
class Csv
|
8
|
+
|
9
|
+
def initialize(report)
|
10
|
+
@report = report
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate
|
14
|
+
CSV.generate(force_quotes: true) do |csv|
|
15
|
+
csv << headers
|
16
|
+
rows.each { |row| csv << row }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def headers
|
23
|
+
@report.columns.map do |column|
|
24
|
+
column_with_prefix(column)
|
25
|
+
end.flatten
|
26
|
+
end
|
27
|
+
|
28
|
+
def column_with_prefix(column, prefixes = [])
|
29
|
+
if column.is_a?(Nuntius::Column)
|
30
|
+
[*prefixes, column.label].join(' ')
|
31
|
+
else
|
32
|
+
column.columns.map do |local_column|
|
33
|
+
column_with_prefix(local_column, prefixes + [column.label])
|
34
|
+
end.flatten
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def rows
|
39
|
+
@report.rows.map do |row|
|
40
|
+
row(row)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def row(row)
|
45
|
+
row.map do |cell|
|
46
|
+
cell
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|