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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +22 -0
  4. data/app/assets/javascripts/nuntius/application.js +39 -0
  5. data/app/assets/javascripts/nuntius/columns.js +57 -0
  6. data/app/assets/javascripts/nuntius/filters/select.js +88 -0
  7. data/app/assets/javascripts/nuntius/filters.js +44 -0
  8. data/app/assets/javascripts/nuntius/forms.js +38 -0
  9. data/app/assets/javascripts/nuntius/nested.js +25 -0
  10. data/app/assets/javascripts/nuntius/reports.js +7 -0
  11. data/app/assets/stylesheets/nuntius/application.css +36 -0
  12. data/app/assets/stylesheets/nuntius/nested.css +8 -0
  13. data/app/assets/stylesheets/nuntius/reports.css +17 -0
  14. data/app/controllers/concerns/nuntius/filter_params.rb +23 -0
  15. data/app/controllers/nuntius/application_controller.rb +17 -0
  16. data/app/controllers/nuntius/reports_controller.rb +45 -0
  17. data/app/helpers/concerns/nuntius/filter_wrappers.rb +86 -0
  18. data/app/helpers/nuntius/alerts_helper.rb +48 -0
  19. data/app/helpers/nuntius/application_helper.rb +19 -0
  20. data/app/helpers/nuntius/cells_helper.rb +24 -0
  21. data/app/helpers/nuntius/columns_helper.rb +47 -0
  22. data/app/helpers/nuntius/filters_helper.rb +147 -0
  23. data/app/helpers/nuntius/forms_helper.rb +18 -0
  24. data/app/helpers/nuntius/rows_helper.rb +21 -0
  25. data/app/views/layouts/nuntius/application.html.erb +21 -0
  26. data/app/views/nuntius/reports/_filter.erb +10 -0
  27. data/app/views/nuntius/reports/_form.erb +26 -0
  28. data/app/views/nuntius/reports/_table.erb +21 -0
  29. data/app/views/nuntius/reports/index.erb +0 -0
  30. data/app/views/nuntius/reports/nested.erb +1 -0
  31. data/app/views/nuntius/reports/show.erb +8 -0
  32. data/app/views/shared/nuntius/_header.erb +16 -0
  33. data/config/initializers/assets.rb +1 -0
  34. data/config/routes.rb +5 -0
  35. data/lib/nuntius/column.rb +64 -0
  36. data/lib/nuntius/columns_group.rb +38 -0
  37. data/lib/nuntius/engine.rb +15 -0
  38. data/lib/nuntius/extensions/date_query.rb +62 -0
  39. data/lib/nuntius/filter.rb +46 -0
  40. data/lib/nuntius/post_processors/date_ranges.rb +120 -0
  41. data/lib/nuntius/report/dsl/columns.rb +132 -0
  42. data/lib/nuntius/report/dsl/filters.rb +50 -0
  43. data/lib/nuntius/report/dsl/nested.rb +66 -0
  44. data/lib/nuntius/report/dsl/post_processors.rb +40 -0
  45. data/lib/nuntius/report/dsl/validations.rb +40 -0
  46. data/lib/nuntius/report/dsl.rb +42 -0
  47. data/lib/nuntius/report/interpolator.rb +75 -0
  48. data/lib/nuntius/report.rb +81 -0
  49. data/lib/nuntius/serializers/csv.rb +54 -0
  50. data/lib/nuntius/validation.rb +30 -0
  51. data/lib/nuntius/version.rb +5 -0
  52. data/lib/nuntius.rb +13 -0
  53. data/lib/tasks/nuntius_tasks.rake +4 -0
  54. 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