reports_kits 0.7.5 → 0.7.7
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 +4 -4
- data/.gitignore +6 -0
- data/.rubocop.yml +85 -0
- data/.travis.yml +21 -0
- data/Appraisals +27 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +35 -0
- data/Rakefile +2 -0
- data/app/assets/javascripts/reports_kits/application.js +14 -0
- data/app/assets/javascripts/reports_kits/lib/_init.js +9 -0
- data/app/assets/javascripts/reports_kits/lib/chart.js +73 -0
- data/app/assets/javascripts/reports_kits/lib/report.js +135 -0
- data/app/assets/javascripts/reports_kits/lib/table.js +87 -0
- data/app/assets/javascripts/reports_kits/vendor/chart.js +12269 -0
- data/app/assets/javascripts/reports_kits/vendor/daterangepicker.js +1627 -0
- data/app/assets/javascripts/reports_kits/vendor/jquery.tablesorter.min.js +4 -0
- data/app/assets/javascripts/reports_kits/vendor/moment.js +4040 -0
- data/app/assets/javascripts/reports_kits/vendor/select2.full.js +6436 -0
- data/app/assets/stylesheets/reports_kits/application.css.scss +3 -0
- data/app/assets/stylesheets/reports_kits/reports.css.sass +33 -0
- data/app/assets/stylesheets/reports_kits/select2_overrides.css.sass +7 -0
- data/app/assets/stylesheets/reports_kits/vendor/daterangepicker.css +269 -0
- data/app/assets/stylesheets/reports_kits/vendor/select2-bootstrap.css +721 -0
- data/app/assets/stylesheets/reports_kits/vendor/select2.css +484 -0
- data/config/initializers/mime_types.rb +1 -0
- data/config/routes.rb +10 -0
- data/docs/images/demo.gif +0 -0
- data/docs/images/users_by_created_at.png +0 -0
- data/gemfiles/mysql.gemfile +7 -0
- data/gemfiles/mysql.gemfile.lock +167 -0
- data/gemfiles/postgresql.gemfile +7 -0
- data/gemfiles/postgresql.gemfile.lock +165 -0
- data/gemfiles/postgresql_rails_5.1.4.gemfile +8 -0
- data/gemfiles/postgresql_rails_5.1.4.gemfile.lock +168 -0
- data/gemfiles/rails_4_mysql.gemfile +8 -0
- data/gemfiles/rails_4_mysql.gemfile.lock +165 -0
- data/gemfiles/rails_4_postgresql.gemfile +8 -0
- data/gemfiles/rails_4_postgresql.gemfile.lock +163 -0
- data/gemfiles/rails_5.1.4_postgresql.gemfile +8 -0
- data/gemfiles/rails_5.1.4_postgresql.gemfile.lock +169 -0
- data/gemfiles/rails_5_mysql.gemfile +8 -0
- data/gemfiles/rails_5_mysql.gemfile.lock +171 -0
- data/gemfiles/rails_5_postgresql.gemfile +8 -0
- data/gemfiles/rails_5_postgresql.gemfile.lock +169 -0
- data/lib/reports_kits/base_controller.rb +17 -0
- data/lib/reports_kits/cache.rb +37 -0
- data/lib/reports_kits/configuration.rb +50 -0
- data/lib/reports_kits/engine.rb +21 -0
- data/lib/reports_kits/entity.rb +3 -0
- data/lib/reports_kits/filters_controller.rb +11 -0
- data/lib/reports_kits/form_builder.rb +66 -0
- data/lib/reports_kits/helper.rb +24 -0
- data/lib/reports_kits/model.rb +16 -0
- data/lib/reports_kits/model_configuration.rb +28 -0
- data/lib/reports_kits/normalized_params.rb +16 -0
- data/lib/reports_kits/order.rb +33 -0
- data/lib/reports_kits/relative_time.rb +42 -0
- data/lib/reports_kits/report_builder.rb +88 -0
- data/lib/reports_kits/reports/abstract_series.rb +9 -0
- data/lib/reports_kits/reports/adapters/mysql.rb +26 -0
- data/lib/reports_kits/reports/adapters/postgresql.rb +26 -0
- data/lib/reports_kits/reports/composite_series.rb +48 -0
- data/lib/reports_kits/reports/contextual_filter.rb +19 -0
- data/lib/reports_kits/reports/data/add_table_aggregations.rb +105 -0
- data/lib/reports_kits/reports/data/aggregate_composite.rb +97 -0
- data/lib/reports_kits/reports/data/aggregate_one_dimension.rb +39 -0
- data/lib/reports_kits/reports/data/aggregate_two_dimensions.rb +39 -0
- data/lib/reports_kits/reports/data/chart_data_for_data_method.rb +62 -0
- data/lib/reports_kits/reports/data/chart_options.rb +155 -0
- data/lib/reports_kits/reports/data/format_one_dimension.rb +140 -0
- data/lib/reports_kits/reports/data/format_table.rb +63 -0
- data/lib/reports_kits/reports/data/format_two_dimensions.rb +143 -0
- data/lib/reports_kits/reports/data/generate.rb +156 -0
- data/lib/reports_kits/reports/data/generate_for_properties.rb +97 -0
- data/lib/reports_kits/reports/data/normalize_properties.rb +62 -0
- data/lib/reports_kits/reports/data/populate_one_dimension.rb +54 -0
- data/lib/reports_kits/reports/data/populate_two_dimensions.rb +104 -0
- data/lib/reports_kits/reports/data/utils.rb +178 -0
- data/lib/reports_kits/reports/dimension.rb +27 -0
- data/lib/reports_kits/reports/dimension_with_series.rb +144 -0
- data/lib/reports_kits/reports/filter.rb +29 -0
- data/lib/reports_kits/reports/filter_types/base.rb +48 -0
- data/lib/reports_kits/reports/filter_types/boolean.rb +47 -0
- data/lib/reports_kits/reports/filter_types/datetime.rb +51 -0
- data/lib/reports_kits/reports/filter_types/number.rb +30 -0
- data/lib/reports_kits/reports/filter_types/records.rb +26 -0
- data/lib/reports_kits/reports/filter_types/string.rb +38 -0
- data/lib/reports_kits/reports/filter_with_series.rb +97 -0
- data/lib/reports_kits/reports/generate_autocomplete_method_results.rb +29 -0
- data/lib/reports_kits/reports/generate_autocomplete_results.rb +57 -0
- data/lib/reports_kits/reports/inferrable_configuration.rb +116 -0
- data/lib/reports_kits/reports/model_settings.rb +30 -0
- data/lib/reports_kits/reports/properties.rb +10 -0
- data/lib/reports_kits/reports/properties_to_filter.rb +40 -0
- data/lib/reports_kits/reports/series.rb +121 -0
- data/lib/reports_kits/reports_controller.rb +65 -0
- data/lib/reports_kits/utils.rb +11 -0
- data/lib/reports_kits/value.rb +3 -0
- data/lib/reports_kits/version.rb +3 -0
- data/lib/reports_kits.rb +79 -0
- data/reports_kits.gemspec +26 -0
- data/spec/factories/issue_factory.rb +4 -0
- data/spec/factories/issues_label_factory.rb +4 -0
- data/spec/factories/label_factory.rb +4 -0
- data/spec/factories/pro_repo_factory.rb +5 -0
- data/spec/factories/repo_factory.rb +5 -0
- data/spec/factories/tag_factory.rb +4 -0
- data/spec/fixtures/generate_inputs.yml +254 -0
- data/spec/fixtures/generate_outputs.yml +1216 -0
- data/spec/reports_kit/form_builder_spec.rb +26 -0
- data/spec/reports_kit/relative_time_spec.rb +29 -0
- data/spec/reports_kit/reports/data/generate/contextual_filters_spec.rb +60 -0
- data/spec/reports_kit/reports/data/generate_spec.rb +1371 -0
- data/spec/reports_kit/reports/data/normalize_properties_spec.rb +196 -0
- data/spec/reports_kit/reports/dimension_with_series_spec.rb +67 -0
- data/spec/reports_kit/reports/filter_with_series_spec.rb +39 -0
- data/spec/reports_kit/reports/generate_autocomplete_results_spec.rb +69 -0
- data/spec/spec_helper.rb +77 -0
- data/spec/support/config.rb +41 -0
- data/spec/support/example_data_methods.rb +25 -0
- data/spec/support/factory_girl.rb +5 -0
- data/spec/support/helpers.rb +25 -0
- data/spec/support/models/issue.rb +14 -0
- data/spec/support/models/issues_label.rb +4 -0
- data/spec/support/models/label.rb +5 -0
- data/spec/support/models/pro/repo.rb +5 -0
- data/spec/support/models/pro/special_issue.rb +4 -0
- data/spec/support/models/repo.rb +13 -0
- data/spec/support/models/tag.rb +4 -0
- data/spec/support/schema.rb +39 -0
- metadata +134 -4
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
class FormBuilder
|
|
3
|
+
include ActionView::Helpers
|
|
4
|
+
|
|
5
|
+
DEFAULT_DATE_RANGE_VALUE = ['-2M', 'now']
|
|
6
|
+
|
|
7
|
+
attr_accessor :properties, :additional_params, :context_record, :properties_to_filter
|
|
8
|
+
|
|
9
|
+
def initialize(properties, additional_params: nil, context_record: nil)
|
|
10
|
+
self.properties = properties.deep_symbolize_keys
|
|
11
|
+
self.additional_params = additional_params
|
|
12
|
+
self.context_record = context_record
|
|
13
|
+
self.properties_to_filter = Reports::PropertiesToFilter.new(properties, context_record: context_record)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def check_box(filter_key, options = {})
|
|
17
|
+
filter = properties_to_filter.perform(filter_key)
|
|
18
|
+
checked = options.key?(:value) ? options[:value] : filter.normalized_properties[:criteria].try(:[], :value) == 'true'
|
|
19
|
+
check_box_tag(filter_key, '1', checked, options)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def date_range(filter_key, options = {})
|
|
23
|
+
filter = properties_to_filter.perform(filter_key)
|
|
24
|
+
defaults = { class: 'form-control input-sm date_range_picker' }
|
|
25
|
+
options = defaults.deep_merge(options)
|
|
26
|
+
value = options[:value].presence || filter.normalized_properties[:criteria].try(:[], :value).presence
|
|
27
|
+
value ||= default_date_range_value
|
|
28
|
+
text_field_tag(filter_key, value, options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def multi_autocomplete(filter_key, options = {})
|
|
32
|
+
filter = properties_to_filter.perform(filter_key)
|
|
33
|
+
reports_kit_path = Rails.application.routes.url_helpers.reports_kit_path
|
|
34
|
+
path = "#{reports_kit_path}reports_kit/filters/#{filter_key}/autocomplete?"
|
|
35
|
+
path += additional_params.to_query if additional_params.present?
|
|
36
|
+
|
|
37
|
+
defaults = {
|
|
38
|
+
class: 'form-control input-sm select2',
|
|
39
|
+
multiple: 'multiple',
|
|
40
|
+
data: {
|
|
41
|
+
placeholder: options[:placeholder],
|
|
42
|
+
path: path
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
options = defaults.deep_merge(options)
|
|
46
|
+
select_tag(filter_key, nil, options)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def string_filter(filter_key, options = {})
|
|
50
|
+
filter = properties_to_filter.perform(filter_key)
|
|
51
|
+
defaults = { class: 'form-control input-sm' }
|
|
52
|
+
options = defaults.deep_merge(options)
|
|
53
|
+
text_field_tag(filter_key, filter.normalized_properties[:criteria].try(:[], :value), options)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def default_date_range_value
|
|
59
|
+
@default_date_range_value ||= begin
|
|
60
|
+
start_date = Reports::Data::Utils.format_time_value(DEFAULT_DATE_RANGE_VALUE[0])
|
|
61
|
+
end_date = Reports::Data::Utils.format_time_value(DEFAULT_DATE_RANGE_VALUE[1])
|
|
62
|
+
[start_date, Reports::FilterTypes::Datetime::SEPARATOR, end_date].join(' ')
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
module Helper
|
|
3
|
+
include ReportsKits::NormalizedParams
|
|
4
|
+
|
|
5
|
+
def render_report(report_params, context_params: {}, actions: %w(export_csv export_xls), js_report_class: 'Report', &block)
|
|
6
|
+
report_params = { key: report_params } if report_params.is_a?(String)
|
|
7
|
+
params.merge!(context_params: context_params, report_params: report_params)
|
|
8
|
+
properties = Reports::Properties.generate(self)
|
|
9
|
+
builder = ReportBuilder.new(
|
|
10
|
+
report_params: report_params,
|
|
11
|
+
context_params: context_params,
|
|
12
|
+
actions: actions,
|
|
13
|
+
js_report_class: js_report_class,
|
|
14
|
+
properties: properties,
|
|
15
|
+
view_context: self,
|
|
16
|
+
block: block
|
|
17
|
+
)
|
|
18
|
+
capture do
|
|
19
|
+
capture(builder, &block) if block
|
|
20
|
+
builder.render
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
module Model
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
class << self
|
|
7
|
+
attr_accessor :reports_kit_configuration
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.reports_kit(&block)
|
|
11
|
+
self.reports_kit_configuration = ModelConfiguration.new
|
|
12
|
+
reports_kit_configuration.instance_eval(&block)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
class ModelConfiguration
|
|
3
|
+
attr_accessor :aggregations, :contextual_filters, :dimensions, :filters
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
self.aggregations = []
|
|
7
|
+
self.contextual_filters = []
|
|
8
|
+
self.dimensions = []
|
|
9
|
+
self.filters = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def aggregation(key, expression, properties = {})
|
|
13
|
+
aggregations << { key: key.to_s, expression: expression }.merge(properties).symbolize_keys
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def contextual_filter(key, method)
|
|
17
|
+
contextual_filters << { key: key, method: method }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def dimension(key, properties)
|
|
21
|
+
dimensions << { key: key.to_s }.merge(properties).symbolize_keys
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def filter(key, type_key, properties)
|
|
25
|
+
filters << { key: key.to_s, type_key: type_key }.merge(properties).symbolize_keys
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
module NormalizedParams
|
|
3
|
+
def report_params
|
|
4
|
+
params[:report_params]
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def context_params
|
|
8
|
+
params[:context_params]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def report_key
|
|
12
|
+
raise ArgumentError.new('Blank report_params') if report_params.blank?
|
|
13
|
+
report_params[:key]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
class Order
|
|
3
|
+
attr_accessor :relation, :field, :direction
|
|
4
|
+
|
|
5
|
+
VALID_RELATIONS = %w(count dimension1 dimension2)
|
|
6
|
+
VALID_FIELDS = [nil, 'label']
|
|
7
|
+
VALID_DIRECTIONS = %w(asc desc)
|
|
8
|
+
|
|
9
|
+
def initialize(relation, field, direction)
|
|
10
|
+
self.relation = relation
|
|
11
|
+
self.field = field
|
|
12
|
+
self.direction = direction
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.parse(string)
|
|
16
|
+
string ||= ''
|
|
17
|
+
field_expression, direction = string.to_s.split(/\s+/)
|
|
18
|
+
relation, field = (field_expression || '').split('.')
|
|
19
|
+
|
|
20
|
+
relation = relation.presence
|
|
21
|
+
field = field.presence
|
|
22
|
+
direction = direction.presence || 'asc'
|
|
23
|
+
|
|
24
|
+
relation = relation.to_i if relation =~ /^\d+$/
|
|
25
|
+
|
|
26
|
+
raise ArgumentError.new("Invalid relation: #{relation}") unless VALID_RELATIONS.include?(relation) || relation.is_a?(Fixnum)
|
|
27
|
+
raise ArgumentError.new("Invalid field: #{field}") unless VALID_FIELDS.include?(field)
|
|
28
|
+
raise ArgumentError.new("Invalid direction: #{direction}") unless VALID_DIRECTIONS.include?(direction)
|
|
29
|
+
|
|
30
|
+
new(relation, field, direction)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
class RelativeTime
|
|
3
|
+
LETTERS_DURATION_METHODS = {
|
|
4
|
+
'y' => :years,
|
|
5
|
+
'M' => :months,
|
|
6
|
+
'w' => :weeks,
|
|
7
|
+
'd' => :days,
|
|
8
|
+
'h' => :hours,
|
|
9
|
+
'm' => :minutes,
|
|
10
|
+
's' => :seconds
|
|
11
|
+
}
|
|
12
|
+
LETTERS = LETTERS_DURATION_METHODS.keys.join
|
|
13
|
+
|
|
14
|
+
def self.parse(string, prevent_exceptions: false)
|
|
15
|
+
return Time.zone.now if string == 'now'
|
|
16
|
+
original_string = string
|
|
17
|
+
string = string.to_s.strip
|
|
18
|
+
is_negative = string[0, 1] == '-'
|
|
19
|
+
string = string[1..-1] if is_negative
|
|
20
|
+
|
|
21
|
+
result_string = is_negative ? '-' : ''
|
|
22
|
+
result_durations = []
|
|
23
|
+
|
|
24
|
+
string.scan(/(\d+)([#{LETTERS}]?)/) do |number, letter|
|
|
25
|
+
result_string += "#{number}#{letter}"
|
|
26
|
+
duration_method = LETTERS_DURATION_METHODS[letter]
|
|
27
|
+
unless duration_method
|
|
28
|
+
return if prevent_exceptions
|
|
29
|
+
raise ArgumentError.new("Invalid duration letter: #{letter.inspect}")
|
|
30
|
+
end
|
|
31
|
+
result_durations << number.to_i.public_send(duration_method)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if result_string == '-' || result_string != original_string.to_s.strip
|
|
35
|
+
return if prevent_exceptions
|
|
36
|
+
raise ArgumentError.new("Invalid time duration: #{original_string.inspect}")
|
|
37
|
+
end
|
|
38
|
+
duration = result_durations.reduce(&:+)
|
|
39
|
+
is_negative ? duration.ago : duration.from_now
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
class ReportBuilder
|
|
3
|
+
include ActionView::Helpers
|
|
4
|
+
|
|
5
|
+
attr_accessor :report_params, :context_params, :additional_params, :actions, :js_report_class, :properties, :view_context, :block, :form_builder
|
|
6
|
+
|
|
7
|
+
ACTION_KEYS_METHODS = {
|
|
8
|
+
'export_csv' => :export_csv_button,
|
|
9
|
+
'export_xls' => :export_xls_button
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def initialize(report_params:, context_params: {}, actions: %w(export_csv export_xls), js_report_class: 'Report', properties:, view_context:, block: nil)
|
|
13
|
+
self.report_params = report_params.is_a?(String) ? { key: report_params } : report_params
|
|
14
|
+
self.context_params = context_params
|
|
15
|
+
self.additional_params = { context_params: context_params, report_params: self.report_params }
|
|
16
|
+
self.actions = actions
|
|
17
|
+
self.js_report_class = js_report_class
|
|
18
|
+
self.view_context = view_context
|
|
19
|
+
self.block = block
|
|
20
|
+
self.properties = properties
|
|
21
|
+
context_record = ReportsKits.configuration.context_record(view_context)
|
|
22
|
+
self.form_builder = ReportsKits::FormBuilder.new(properties, additional_params: additional_params, context_record: context_record)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def render
|
|
26
|
+
data = { properties: properties.slice(:format), path: reports_data_path, report_class: js_report_class }
|
|
27
|
+
view_context.content_tag :div, nil, class: 'reports_kit_report form-inline', data: data do
|
|
28
|
+
elements = []
|
|
29
|
+
elements << view_context.capture(self, &block) if block
|
|
30
|
+
elements << view_context.content_tag(:div, nil, class: 'reports_kit_visualization')
|
|
31
|
+
elements << action_elements_container
|
|
32
|
+
elements.compact.join.html_safe
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def form(&block)
|
|
37
|
+
raise ArgumentError.new('No block given for ReportBuilder#form') unless block
|
|
38
|
+
view_context.form_tag(reports_data_path, method: 'get', class: 'reports_kit_report_form') do
|
|
39
|
+
view_context.capture(form_builder, &block)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def export_csv_button(text = 'Download CSV', options = {}, &block)
|
|
44
|
+
export_button(text, 'csv', options, &block)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def export_xls_button(text = 'Download Excel', options = {}, &block)
|
|
48
|
+
export_button(text, 'xls', options, &block)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def export_button(text, format, options, &block)
|
|
52
|
+
data = {
|
|
53
|
+
role: 'reports_kit_export_button',
|
|
54
|
+
path: view_context.reports_kit.reports_kit_reports_path({ format: format }.merge(additional_params))
|
|
55
|
+
}
|
|
56
|
+
options = { class: 'btn btn-primary', data: data }.merge(options)
|
|
57
|
+
if block_given?
|
|
58
|
+
view_context.link_to('#', options, &block)
|
|
59
|
+
else
|
|
60
|
+
view_context.link_to(text, '#', options)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def reports_data_path
|
|
67
|
+
@reports_data_path ||= view_context.reports_kit.reports_kit_reports_path({ format: 'json' }.merge(additional_params))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def action_elements_container
|
|
71
|
+
return if action_elements.blank?
|
|
72
|
+
view_context.content_tag(:div, nil, class: 'reports_kit_actions') do
|
|
73
|
+
action_elements.map { |element| view_context.concat(element) }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def action_elements
|
|
78
|
+
@action_elements ||= begin
|
|
79
|
+
return if actions.blank?
|
|
80
|
+
actions.map do |action|
|
|
81
|
+
element_method = ACTION_KEYS_METHODS[action]
|
|
82
|
+
raise ArgumentError.new("Invalid action: #{action}") unless element_method
|
|
83
|
+
send(element_method)
|
|
84
|
+
end.compact
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
module Reports
|
|
3
|
+
module Adapters
|
|
4
|
+
class Mysql
|
|
5
|
+
def self.truncate_to_day(column)
|
|
6
|
+
"DATE(#{column})"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.truncate_to_week(column)
|
|
10
|
+
case ReportsKits.configuration.first_day_of_week
|
|
11
|
+
when :sunday
|
|
12
|
+
"DATE_SUB(DATE(#{column}), INTERVAL DAYOFWEEK(#{column}) - 1 DAY)"
|
|
13
|
+
when :monday
|
|
14
|
+
"DATE_SUB(DATE(#{column}), INTERVAL DAYOFWEEK(#{column}) - 2 DAY)"
|
|
15
|
+
else
|
|
16
|
+
raise ArgumentError.new("Unsupported first_day_of_week: #{ReportsKits.configuration.first_day_of_week}")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.truncate_to_month(column)
|
|
21
|
+
"DATE_SUB(DATE(#{column}), INTERVAL DAY(#{column}) - 1 DAY)"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
module Reports
|
|
3
|
+
module Adapters
|
|
4
|
+
class Postgresql
|
|
5
|
+
def self.truncate_to_day(column)
|
|
6
|
+
"#{column}::date"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.truncate_to_week(column)
|
|
10
|
+
case ReportsKits.configuration.first_day_of_week
|
|
11
|
+
when :sunday
|
|
12
|
+
"DATE_TRUNC('week', #{column}::timestamp + '1 day'::interval) - '1 day'::interval"
|
|
13
|
+
when :monday
|
|
14
|
+
"DATE_TRUNC('week', #{column}::timestamp)"
|
|
15
|
+
else
|
|
16
|
+
raise ArgumentError.new("Unsupported first_day_of_week: #{ReportsKits.configuration.first_day_of_week}")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.truncate_to_month(column)
|
|
21
|
+
"DATE_TRUNC('month', #{column}::date)"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
module Reports
|
|
3
|
+
class CompositeSeries < AbstractSeries
|
|
4
|
+
attr_accessor :properties, :context_record
|
|
5
|
+
|
|
6
|
+
def initialize(properties, context_record:)
|
|
7
|
+
self.properties = properties.dup
|
|
8
|
+
self.context_record = context_record
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def label
|
|
12
|
+
name
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def name
|
|
16
|
+
properties[:name]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def composite_operator
|
|
20
|
+
properties[:composite_operator]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def limit
|
|
24
|
+
properties[:limit]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def serieses
|
|
28
|
+
@serieses ||= Reports::Series.new_from_properties!(properties, context_record: context_record)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def filters
|
|
32
|
+
serieses.map(&:filters).flatten
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def primary_series
|
|
36
|
+
serieses.first
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def dimensions
|
|
40
|
+
primary_series.dimensions
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def model_class
|
|
44
|
+
primary_series.model_class
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
module Reports
|
|
3
|
+
class ContextualFilter
|
|
4
|
+
attr_accessor :key, :model_settings
|
|
5
|
+
|
|
6
|
+
delegate :settings_from_model, to: :model_settings
|
|
7
|
+
|
|
8
|
+
def initialize(key, model_class)
|
|
9
|
+
self.key = key.to_sym
|
|
10
|
+
self.model_settings = ModelSettings.new(model_class, :contextual_filters, self.key)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def apply(relation, context_params)
|
|
14
|
+
raise ArgumentError.new("contextual_filter with key :#{key} not defined in #{model_class}") if settings_from_model.blank?
|
|
15
|
+
settings_from_model[:method].call(relation, context_params)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
module Reports
|
|
3
|
+
module Data
|
|
4
|
+
class AddTableAggregations
|
|
5
|
+
attr_accessor :data, :report_options
|
|
6
|
+
|
|
7
|
+
VALID_AGGREGATION_OPERATORS = [:sum]
|
|
8
|
+
|
|
9
|
+
def initialize(data, report_options:)
|
|
10
|
+
self.data = data
|
|
11
|
+
self.report_options = report_options || {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def perform
|
|
15
|
+
data_with_aggregations
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def data_with_aggregations
|
|
21
|
+
return data if row_aggregation_configs.blank? && column_aggregation_configs.blank?
|
|
22
|
+
{
|
|
23
|
+
entities: entities,
|
|
24
|
+
datasets: datasets
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def entities
|
|
29
|
+
column_aggregation_entities = column_aggregation_configs.map do |config|
|
|
30
|
+
ReportsKits::Entity.new(config[:label], config[:label], config[:label])
|
|
31
|
+
end
|
|
32
|
+
entities_with_row_aggregations + column_aggregation_entities
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def datasets
|
|
36
|
+
datasets_with_row_aggregations.map do |dataset|
|
|
37
|
+
column_aggregation_configs.each do |config|
|
|
38
|
+
value = aggregate_array(dataset[:values].map(&:formatted), config[:operator])
|
|
39
|
+
dataset[:values] << ReportsKits::Value.new(value, value)
|
|
40
|
+
end
|
|
41
|
+
dataset
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def entities_with_row_aggregations
|
|
46
|
+
@entities_with_row_aggregations ||= begin
|
|
47
|
+
return original_entities if row_aggregation_configs.blank?
|
|
48
|
+
row_aggregation_entities = row_aggregation_configs.map do |config|
|
|
49
|
+
ReportsKits::Entity.new(config[:label], config[:label], config[:label])
|
|
50
|
+
end
|
|
51
|
+
original_entities + row_aggregation_entities
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def datasets_with_row_aggregations
|
|
56
|
+
@datasets_with_row_aggregations ||= begin
|
|
57
|
+
return original_datasets if row_aggregation_configs.blank?
|
|
58
|
+
row_aggregation_datasets = row_aggregation_configs.map do |config|
|
|
59
|
+
values = original_datasets.map { |dataset| dataset[:values].map(&:formatted) }.transpose
|
|
60
|
+
aggregated_values = aggregate_array_of_arrays(values, config[:operator])
|
|
61
|
+
values = aggregated_values.map { |value| ReportsKits::Value.new(value, value) }
|
|
62
|
+
{
|
|
63
|
+
entity: ReportsKits::Entity.new(config[:label], config[:label], config[:label]),
|
|
64
|
+
values: values
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
original_datasets + row_aggregation_datasets
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def original_entities
|
|
72
|
+
data[:entities]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def original_datasets
|
|
76
|
+
data[:datasets]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def row_aggregation_configs
|
|
80
|
+
return [] if report_options[:aggregations].blank?
|
|
81
|
+
report_options[:aggregations].select { |config| config[:from] == 'rows' }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def column_aggregation_configs
|
|
85
|
+
return [] if report_options[:aggregations].blank?
|
|
86
|
+
report_options[:aggregations].select { |config| config[:from] == 'columns' }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def aggregate_array(values, operator)
|
|
90
|
+
operator = operator.try(:to_sym)
|
|
91
|
+
raise ArgumentError.new("Invalid aggregation operator: #{operator}") unless operator.in?(VALID_AGGREGATION_OPERATORS)
|
|
92
|
+
if values.first.is_a?(Numeric)
|
|
93
|
+
values.public_send(operator)
|
|
94
|
+
else
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def aggregate_array_of_arrays(array_of_arrays, operator)
|
|
100
|
+
array_of_arrays.map { |values| aggregate_array(values, operator) }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module ReportsKits
|
|
2
|
+
module Reports
|
|
3
|
+
module Data
|
|
4
|
+
class AggregateComposite
|
|
5
|
+
attr_accessor :composite_series, :context_record
|
|
6
|
+
|
|
7
|
+
delegate :composite_operator, :properties, to: :composite_series
|
|
8
|
+
|
|
9
|
+
OPERATORS_METHODS = {
|
|
10
|
+
'+' => :+,
|
|
11
|
+
'-' => :-,
|
|
12
|
+
'*' => :*,
|
|
13
|
+
'/' => :/,
|
|
14
|
+
'%' => -> (values) {
|
|
15
|
+
raise ArgumentError.new('Percentage composite aggregations must have exactly two series') if values.length != 2
|
|
16
|
+
numerator, denominator = values
|
|
17
|
+
return 0 if denominator == 0
|
|
18
|
+
((numerator.to_f / denominator) * 100).round(1)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def initialize(properties, context_record:)
|
|
23
|
+
self.composite_series = CompositeSeries.new(properties, context_record: context_record)
|
|
24
|
+
self.context_record = context_record
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def perform
|
|
28
|
+
return serieses_results_for_one_dimension if dimension_count == 1
|
|
29
|
+
return serieses_results_for_two_dimensions if dimension_count == 2
|
|
30
|
+
raise ArgumentError.new("Composite aggregations' series can only have 1-2 dimensions")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def serieses_results_for_one_dimension
|
|
36
|
+
serieses_results = Hash[serieses.map { |series| [series, AggregateOneDimension.new(series).perform] }]
|
|
37
|
+
serieses_results = Data::PopulateOneDimension.new(serieses_results, context_record: context_record, properties: properties).perform
|
|
38
|
+
sorted_dimension_keys_values = sort_dimension_keys_values(serieses_results)
|
|
39
|
+
value_lists = sorted_dimension_keys_values.map(&:values)
|
|
40
|
+
composited_values = value_lists.transpose.map { |data| reduce(data) }
|
|
41
|
+
dimension_keys = sorted_dimension_keys_values.first.keys
|
|
42
|
+
composited_keys_values = dimension_keys.zip(composited_values)
|
|
43
|
+
Hash[composited_keys_values]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def serieses_results_for_two_dimensions
|
|
47
|
+
serieses_results = Hash[serieses.map { |series| [series, AggregateTwoDimensions.new(series).perform] }]
|
|
48
|
+
serieses_results = Data::PopulateTwoDimensions.new(serieses_results).perform
|
|
49
|
+
value_lists = serieses_results.values.map(&:values)
|
|
50
|
+
composited_values = value_lists.transpose.map { |data| reduce(data) }
|
|
51
|
+
dimension_keys = serieses_results.values.first.keys
|
|
52
|
+
composited_keys_values = dimension_keys.zip(composited_values)
|
|
53
|
+
Hash[composited_keys_values]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Before performing a composition of values, we need to make sure that the values are sorted by the same dimension key.
|
|
57
|
+
def sort_dimension_keys_values(serieses_results)
|
|
58
|
+
dimension_keys_values_list = serieses_results.values
|
|
59
|
+
sorted_dimension_keys_values = dimension_keys_values_list.map do |dimension_keys_values|
|
|
60
|
+
dimension_keys_values = dimension_keys_values.sort_by do |dimension_key, value|
|
|
61
|
+
is_boolean = dimension_key.is_a?(TrueClass) || dimension_key.is_a?(FalseClass)
|
|
62
|
+
is_boolean ? (dimension_key ? 0 : 1) : dimension_key
|
|
63
|
+
end
|
|
64
|
+
Hash[dimension_keys_values]
|
|
65
|
+
end
|
|
66
|
+
sorted_dimension_keys_values
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def reduce(values)
|
|
70
|
+
if composite_method.is_a?(Symbol)
|
|
71
|
+
values.reduce(&composite_method)
|
|
72
|
+
elsif composite_method.is_a?(Proc)
|
|
73
|
+
values = composite_method.call(values)
|
|
74
|
+
else
|
|
75
|
+
raise ArgumentError.new("Invalid composite method type: #{composite_method.class}")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def composite_method
|
|
80
|
+
composite_method = OPERATORS_METHODS[composite_operator]
|
|
81
|
+
raise ArgumentError.new("Invalid composite_operator: #{composite_operator}") unless composite_method
|
|
82
|
+
composite_method
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def dimension_count
|
|
86
|
+
unique_dimension_counts = serieses.map { |series| series.dimensions.length }.uniq
|
|
87
|
+
raise ArgumentError.new('All series must have the same number of dimensions') if unique_dimension_counts.length > 1
|
|
88
|
+
unique_dimension_counts.first
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def serieses
|
|
92
|
+
@serieses ||= Series.new_from_properties!(properties, context_record: context_record)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|