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,47 @@
1
+ module Nuntius
2
+
3
+ module ColumnsHelper
4
+
5
+ def column_table_header_tag(column)
6
+ options = column.html_options.deep_merge(colspan: column.html_options.fetch(:colspan, 1),
7
+ rowspan: column.html_options.fetch(:rowspan, 1),
8
+ class: class_for_column_header(column),
9
+ data: data_for_column_header(column))
10
+
11
+ content_tag(:th, column.label, options)
12
+ end
13
+
14
+ def number_of_header_levels(columns)
15
+ columns.map(&:depth).max
16
+ end
17
+
18
+ def header_columns_for_level(columns, level)
19
+ if level == 0
20
+ columns
21
+ else
22
+ columns.select { |column| column.is_a?(ColumnsGroup) }
23
+ .flat_map { |column| column.columns_for_level(level) }
24
+ end
25
+ end
26
+
27
+ def class_for_column_header(column)
28
+ css_class = if column.is_a?(Column)
29
+ 'column-header'
30
+ elsif column.is_a?(ColumnsGroup)
31
+ 'column-group-header'
32
+ end
33
+
34
+ [column.html_options[:class], css_class].compact.join(' ')
35
+ end
36
+
37
+ def data_for_column_header(column)
38
+ if column.is_a?(Column)
39
+ { type: column.type }
40
+ elsif column.is_a?(ColumnsGroup)
41
+ {}
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,147 @@
1
+ # rubocop:disable all
2
+ module Nuntius
3
+
4
+ module FiltersHelper
5
+
6
+ include Nuntius::FilterWrappers
7
+
8
+ RESERVED_OPTIONS = [:dependent]
9
+
10
+ def filter_tag(filter, report, options = {})
11
+ tag_options = set_current_filter_value(filter, report)
12
+ tag_options = merge_filter_options(filter, options, tag_options)
13
+ tag_options = add_filter_options(filter, report, tag_options)
14
+
15
+ send(:"#{filter.type}_filter_tag", scope_name(filter.name), tag_options)
16
+ end
17
+
18
+ def class_for_filter_wrapper(filter)
19
+ classes = ['form-group']
20
+
21
+ if filter.type == :select
22
+ classes << 'select-filter-wrapper'
23
+ end
24
+
25
+ classes.join(' ')
26
+ end
27
+
28
+ def class_for_filter(filter)
29
+ classes = ['nuntius-filter']
30
+
31
+ unless filter.type == :select
32
+ classes << 'form-control'
33
+ end
34
+
35
+ classes.join(' ')
36
+ end
37
+
38
+ def set_current_filter_value(filter, report)
39
+ tag_options = filter.args.dup
40
+ current_value = report.filter_params[filter.name]
41
+
42
+ case filter.type
43
+ when :select
44
+ tag_options[:option_tags] = compile_select_option_tags(filter, report)
45
+ when :check_box
46
+ tag_options[:checked] = current_value == (tag_options[:value] || DEFAULT_CHECK_BOX_VALUE)
47
+ when :radio_button
48
+ tag_options[:checked] = current_value == (tag_options[:value] || DEFAULT_RADIO_BUTTON_VALUE)
49
+ when :text_area
50
+ tag_options[:content] = current_value || tag_options[:content]
51
+ else
52
+ tag_options[:value] = current_value || tag_options[:value]
53
+ end
54
+
55
+ tag_options
56
+ end
57
+
58
+ def compile_select_option_tags(filter, report)
59
+ option_tags = filter.args[:option_tags]
60
+ option_tags = option_tags.is_a?(Symbol) ? report.send(option_tags) : option_tags
61
+ current_value = report.filter_params[filter.name]
62
+
63
+ if filter.args[:dependent].present? && option_tags.is_a?(Hash)
64
+ return ''
65
+ elsif filter.args[:dependent].present?
66
+ fail ArgumentError, 'when a dependent select option_tags can only be a Hash.'
67
+ end
68
+
69
+ if option_tags.is_a?(String)
70
+ option_tags.html_safe
71
+ elsif option_tags.is_a?(Array)
72
+ if option_tags.size == 1 || option_tags.size == 2
73
+ if option_tags.size == 2
74
+ option_tags[1] = current_value || option_tags[1]
75
+ else
76
+ option_tags << current_value
77
+ end
78
+ options_for_select(*option_tags)
79
+ elsif option_tags.size == 3 || option_tags.size == 4
80
+ if option_tags.size == 4
81
+ option_tags[3] = current_value || option_tags[3]
82
+ else
83
+ option_tags << current_value
84
+ end
85
+ options_from_collection_for_select(*option_tags)
86
+ else
87
+ fail ArgumentError, 'option_tags can only be a String, an Array(max size 4) or a Symbol.'
88
+ end
89
+ else
90
+ fail ArgumentError, 'option_tags can only be a String, an Array(max size 4) or a Symbol.'
91
+ end
92
+ end
93
+
94
+ protected
95
+
96
+ def merge_filter_options(filter, options, tag_options)
97
+ tag_options.delete_if { |k, _| RESERVED_OPTIONS.include?(k) }
98
+
99
+ (tag_options[:options] ||= {}).each do |k, v|
100
+ opt = options.delete(k)
101
+ next unless opt
102
+
103
+ case v.class.to_s
104
+ when String.to_s
105
+ v << ' ' << opt
106
+ when Array.to_s
107
+ v.concat(opt)
108
+ when Hash.to_s
109
+ v.merge!(opt)
110
+ end
111
+ end
112
+ tag_options[:options].merge!(options)
113
+
114
+ tag_options
115
+ end
116
+
117
+ def add_filter_options(filter, report, tag_options)
118
+ tag_options = (tag_options || {}).dup
119
+ tag_options[:options] ||= {}
120
+
121
+ if filter.type == :select
122
+ include_blank = (filter.args[:options] || {})[:include_blank]
123
+
124
+ tag_options[:options].deep_merge!(data: { include_blank: include_blank })
125
+ end
126
+
127
+ if filter.type == :select && filter.args[:dependent].present?
128
+ options = filter.args[:option_tags]
129
+ options = options.is_a?(Symbol) ? report.send(options) : options
130
+ current_value = report.filter_params[filter.name]
131
+
132
+ options = options.inject({}) do |result, (key, value)|
133
+ result.merge(Array(key).join('_!_') => value)
134
+ end
135
+
136
+ tag_options[:options].deep_merge!(disabled: true,
137
+ data: { dependent: filter.args[:dependent],
138
+ options: options, current_value: current_value })
139
+ end
140
+
141
+ tag_options
142
+ end
143
+
144
+ end
145
+
146
+ end
147
+ # rubocop:enable all
@@ -0,0 +1,18 @@
1
+ module Nuntius
2
+
3
+ module FormsHelper
4
+
5
+ def compile_filters(filters)
6
+ filters.each_with_object({}) { |filter, memo| memo.merge!(scope_keys(filter.to_js)) }
7
+ end
8
+
9
+ def compile_validations(validations)
10
+ validations.each_with_object(rules: {}, messages: {}) do |validation, memo|
11
+ memo[:rules].merge!(scope_keys(validation.to_js[:rules]))
12
+ memo[:messages].merge!(scope_keys(validation.to_js[:messages]))
13
+ end
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,21 @@
1
+ module Nuntius
2
+
3
+ module RowsHelper
4
+
5
+ def row_tag(row, report, &block)
6
+ options = { data: data_for_row(row, report) }
7
+
8
+ content_tag(:tr, options, &block)
9
+ end
10
+
11
+ def data_for_row(row, report)
12
+ return unless report.enable_nested?
13
+
14
+ { nested: { url: nested_report_path(name: report.class.name),
15
+ row: row,
16
+ filters: { q: report.filter_params } } }
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,21 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Nuntius</title>
5
+ <%= stylesheet_link_tag "nuntius/application", media: "all" %>
6
+ <%= csrf_meta_tags %>
7
+ </head>
8
+ <body>
9
+ <header>
10
+ <%= render partial: 'shared/nuntius/header' %>
11
+ </header>
12
+ <main>
13
+ <div class='container-fluid'>
14
+ <%= flash_alert %>
15
+ <%= yield %>
16
+ </div>
17
+ </main>
18
+ <footer></footer>
19
+ <%= javascript_include_tag 'nuntius/application' %>
20
+ </body>
21
+ </html>
@@ -0,0 +1,10 @@
1
+ <% if filter.options[:icon].present? %>
2
+ <div class="input-group">
3
+ <%= filter_tag filter, report, class: class_for_filter(filter) %>
4
+ <span class="input-group-addon">
5
+ <span class="glyphicon glyphicon-<%= filter.options[:icon] %>"></span>
6
+ </span>
7
+ </div>
8
+ <% else %>
9
+ <%= filter_tag filter, report, class: class_for_filter(filter) %>
10
+ <% end %>
@@ -0,0 +1,26 @@
1
+ <script>
2
+ var FILTERS = <%= raw compile_filters(report.filters).to_json %>;
3
+ var FILTER_PARAMS = <%= raw scope_keys(filter_params).to_json %>;
4
+ var VALIDATIONS = <%= raw compile_validations(report.validations).to_json %>
5
+ </script>
6
+
7
+ <%= form_tag report_path(name: report.class.name), id: 'filter-form', method: :get, class: 'form-inline' do %>
8
+ <%= hidden_field_tag :format, :html %>
9
+
10
+ <% report.filters.each do |filter| %>
11
+ <div class='<%= class_for_filter_wrapper(filter) %>'>
12
+ <%= label_tag filter.name, nil, class: 'control-label' %>
13
+ <%= render partial: 'filter', locals: { filter: filter, report: report } %>
14
+ </div>
15
+ <% end %>
16
+ <div>
17
+ <div class='form-group pull-right'>
18
+ <%= submit_tag 'Filter', class: 'btn btn-success' %>
19
+ <%= link_to '#', class: 'btn btn-default download' do %>
20
+ <i class='glyphicon glyphicon-download-alt'></i>
21
+ Download
22
+ <% end %>
23
+ </div>
24
+ <div class='clearfix'></div>
25
+ </div>
26
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <% nested = local_assigns.fetch(:nested, false) %>
2
+ <table class='table table-hover <%= 'nested' if nested %>'>
3
+ <thead>
4
+ <% number_of_header_levels(report.columns).times do |level| %>
5
+ <tr>
6
+ <% header_columns_for_level(report.columns, level).each do |column| %>
7
+ <%= column_table_header_tag(column) %>
8
+ <% end %>
9
+ </tr>
10
+ <% end %>
11
+ </thead>
12
+ <tbody>
13
+ <% report.processed_rows.each do |row| %>
14
+ <%= row_tag(row, report) do %>
15
+ <% report.columns_without_groups.each do |column|%>
16
+ <%= cell_tag(cell_for_row_column(report, row, column)) %>
17
+ <% end %>
18
+ <% end %>
19
+ <% end %>
20
+ </tbody>
21
+ </table>
File without changes
@@ -0,0 +1 @@
1
+ <%= render partial: 'table', locals: { report: @nested_report, nested: true } %>
@@ -0,0 +1,8 @@
1
+ <div>
2
+ <section class='well well-sm'>
3
+ <%= render partial: 'form', locals: { report: @report } %>
4
+ </section>
5
+ <section class='panel'>
6
+ <%= render partial: 'table', locals: { report: @report, nest: true } %>
7
+ </section>
8
+ </div>
@@ -0,0 +1,16 @@
1
+ <nav class="navbar navbar-default">
2
+ <div class="container-fluid">
3
+ <div class="navbar-header">
4
+ <a class="navbar-brand" href='#'>
5
+ Reports
6
+ </a>
7
+ </div>
8
+ </div>
9
+ <ul class="nav nav-tabs">
10
+ <%- reports.each do |report| %>
11
+ <li class="<%= report == @report.class ? 'active' : '' %>">
12
+ <%= link_to report.tab_title, report_path(name: report.name) %>
13
+ </li>
14
+ <%- end %>
15
+ </ul>
16
+ </nav>
@@ -0,0 +1 @@
1
+ Nuntius::Engine.config.assets.paths << Nuntius::Engine.root.join('vendor', 'assets', 'bower_components')
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ Nuntius::Engine.routes.draw do
2
+ resources :reports, param: :name, only: [:index, :show] do
3
+ get :nested, on: :member
4
+ end
5
+ end
@@ -0,0 +1,64 @@
1
+ module Nuntius
2
+
3
+ class Column
4
+
5
+ TYPES = %i(string integer float datetime date)
6
+
7
+ attr_reader :name, :type, :label
8
+
9
+ # rubocop:disable Metrics/CyclomaticComplexity
10
+ def initialize(name, type, options = {})
11
+ fail ArgumentError, "ColumnType: [#{type}] not yet implemented." unless TYPES.include?(type.to_sym)
12
+
13
+ @name = name.to_sym
14
+ @type = type.to_sym
15
+ @label = options.delete(:label) || @name.to_s
16
+ @label = instance_exec(&@label) if @label.is_a?(Proc)
17
+ @default_value = options[:default_value] || default_value_for_type
18
+ @value = options[:value] || default_value_block(@name, @default_value)
19
+ @options = options
20
+ @children = options[:children] || []
21
+ end
22
+ # rubocop:enable Metrics/CyclomaticComplexity
23
+
24
+ def html_options
25
+ @options[:html_options] || {}
26
+ end
27
+
28
+ def value(report, row)
29
+ if @value.is_a?(Proc)
30
+ report.instance_exec(row, &@value)
31
+ elsif @value.is_a?(Symbol)
32
+ report.public_send(@value, row)
33
+ else
34
+ @value
35
+ end
36
+ end
37
+
38
+ def depth
39
+ 1
40
+ end
41
+
42
+ private
43
+
44
+ def default_value_block(name, default_value)
45
+ lambda do |row|
46
+ if row.is_a?(Hash)
47
+ row[name.to_s] || row[name.to_sym]
48
+ else
49
+ row[column_index(name.to_sym)]
50
+ end || default_value
51
+ end
52
+ end
53
+
54
+ def default_value_for_type
55
+ case @type
56
+ when :integer then 0
57
+ when :float then 0.0
58
+ else ''
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+ end
@@ -0,0 +1,38 @@
1
+ module Nuntius
2
+
3
+ class ColumnsGroup
4
+
5
+ attr_reader :name, :columns, :label
6
+
7
+ def initialize(name, columns, options = {})
8
+ @name = name.to_sym
9
+ @columns = columns
10
+ @label = options.delete(:label) || @name.to_s
11
+ @label = instance_exec(&@label) if @label.is_a?(Proc)
12
+ @options = options
13
+ end
14
+
15
+ def html_options
16
+ @options[:html_options] || {}
17
+ end
18
+
19
+ def deep_dup
20
+ Marshal.load(Marshal.dump(self))
21
+ end
22
+
23
+ def depth
24
+ 1 + (@columns.map(&:depth).max || 0)
25
+ end
26
+
27
+ def columns_for_level(level)
28
+ if level == 1
29
+ @columns
30
+ else
31
+ @columns.select { |column| column.is_a?(ColumnsGroup) }
32
+ .flat_map { |child| child.columns_for_level(level - 1) }
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,15 @@
1
+ module Nuntius
2
+
3
+ class Engine < ::Rails::Engine
4
+
5
+ isolate_namespace Nuntius
6
+
7
+ config.autoload_paths << root.join('app/helpers/concerns/')
8
+
9
+ config.generators do |g|
10
+ g.test_framework :rspec
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -0,0 +1,62 @@
1
+ module Nuntius
2
+
3
+ module Extension
4
+
5
+ module DateQuery
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
+ SQL_FUNCTIONS = {
19
+ 'month' => "DATE_TRUNC('month', <%column%>::timestamptz AT TIME ZONE {time_zone})",
20
+ 'day' => "DATE_TRUNC('day', <%column%>::timestamptz AT TIME ZONE {time_zone})",
21
+ 'dow' => 'EXTRACT(DOW from <%column%>::timestamptz AT TIME ZONE {time_zone})::integer',
22
+ 'hour' => 'EXTRACT(HOUR from <%column%>::timestamptz AT TIME ZONE {time_zone})::integer',
23
+ 'moy' => 'EXTRACT(MONTH from <%column%>::timestamptz AT TIME ZONE {time_zone})::integer'
24
+ }
25
+
26
+ # The different component that can be extracted from a timestamp
27
+ #
28
+ # @return [Hash] The components
29
+ def date_extract_options
30
+ [
31
+ {
32
+ 'Day' => 'day',
33
+ 'Month' => 'month',
34
+ 'Day of week' => 'dow',
35
+ 'Hour of day' => 'hour',
36
+ 'Month of year' => 'moy'
37
+ }
38
+ ]
39
+ end
40
+
41
+ # SQL function for the extraction of the desired timestamp component
42
+ #
43
+ # @param [String] component The Date component to extract
44
+ # @param [String] column The column that has the timestamp
45
+ # @param [String] time_zone The time_zone of the timestamp. Default: 'America/Montevideo'
46
+ #
47
+ # @return [String] The SQL function
48
+ def date_extract(component:, column:, time_zone: 'America/Montevideo')
49
+ sql_function = SQL_FUNCTIONS[component].dup
50
+
51
+ Class.new(OpenStruct) {
52
+ include Nuntius::Report::Interpolator
53
+ }.new(column: column, time_zone: time_zone).interpolate(sql_function)
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,46 @@
1
+ module Nuntius
2
+
3
+ class Filter
4
+
5
+ RESERVED_ARGS = %i(icon priority)
6
+ DEFAULT_RESERVED_ARGS = { priority: 0 }
7
+ TYPES = %i(check_box color date datetime email month number phone radio_button range search select
8
+ telephone text_area text url week)
9
+
10
+ attr_reader :name, :type, :args, :options
11
+
12
+ def initialize(name, type, dirty_args = {})
13
+ fail ArgumentError, "FilterType: [#{type}] not yet implemented." unless TYPES.include?(type.to_sym)
14
+
15
+ @name = name.to_sym
16
+ @type = type.to_sym
17
+ @args = clean_args(dirty_args)
18
+ @options = DEFAULT_RESERVED_ARGS.merge(reserved_args(dirty_args))
19
+ end
20
+
21
+ def to_js
22
+ {
23
+ "#{name}" => {
24
+ type: type,
25
+ options: options
26
+ }
27
+ }
28
+ end
29
+
30
+ def deep_dup
31
+ Marshal.load(Marshal.dump(self))
32
+ end
33
+
34
+ protected
35
+
36
+ def reserved_args(args)
37
+ args.except(*clean_args(args).keys) || {}
38
+ end
39
+
40
+ def clean_args(args)
41
+ args.except(*RESERVED_ARGS) || {}
42
+ end
43
+
44
+ end
45
+
46
+ end