noventius 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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