reports_kit 0.0.1
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/.gitignore +6 -0
- data/.rubocop.yml +83 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +468 -0
- data/Rakefile +2 -0
- data/app/assets/javascripts/reports_kit/application.js +14 -0
- data/app/assets/javascripts/reports_kit/lib/_init.js +8 -0
- data/app/assets/javascripts/reports_kit/lib/chart.js +39 -0
- data/app/assets/javascripts/reports_kit/lib/report.js +119 -0
- data/app/assets/javascripts/reports_kit/vendor/chart.js +12269 -0
- data/app/assets/javascripts/reports_kit/vendor/daterangepicker.js +1627 -0
- data/app/assets/javascripts/reports_kit/vendor/moment.js +4040 -0
- data/app/assets/javascripts/reports_kit/vendor/select2.full.js +6436 -0
- data/app/assets/stylesheets/reports_kit/application.css.scss +3 -0
- data/app/assets/stylesheets/reports_kit/reports.css.sass +7 -0
- data/app/assets/stylesheets/reports_kit/select2_overrides.css.sass +7 -0
- data/app/assets/stylesheets/reports_kit/vendor/daterangepicker.css +269 -0
- data/app/assets/stylesheets/reports_kit/vendor/select2-bootstrap.css +721 -0
- data/app/assets/stylesheets/reports_kit/vendor/select2.css +484 -0
- data/config/routes.rb +10 -0
- data/docs/images/chart_options.png +0 -0
- data/docs/images/dashed_line.png +0 -0
- data/docs/images/flights_by_carrier.png +0 -0
- data/docs/images/flights_by_carrier_and_flight_at.png +0 -0
- data/docs/images/flights_by_delay.png +0 -0
- data/docs/images/flights_by_flight_at.png +0 -0
- data/docs/images/flights_by_hours_delayed.png +0 -0
- data/docs/images/flights_with_check_box.png +0 -0
- data/docs/images/flights_with_configured_boolean.png +0 -0
- data/docs/images/flights_with_configured_datetime.png +0 -0
- data/docs/images/flights_with_configured_number.png +0 -0
- data/docs/images/flights_with_configured_string.png +0 -0
- data/docs/images/flights_with_date_range.png +0 -0
- data/docs/images/flights_with_filters.png +0 -0
- data/docs/images/flights_with_multi_autocomplete.png +0 -0
- data/docs/images/flights_with_string_filter.png +0 -0
- data/docs/images/horizontal_bar.png +0 -0
- data/docs/images/legend_right.png +0 -0
- data/docs/images/users_by_created_at.png +0 -0
- data/gists/doc.txt +58 -0
- data/lib/reports_kit.rb +17 -0
- data/lib/reports_kit/base_controller.rb +11 -0
- data/lib/reports_kit/configuration.rb +10 -0
- data/lib/reports_kit/engine.rb +21 -0
- data/lib/reports_kit/helper.rb +19 -0
- data/lib/reports_kit/model.rb +16 -0
- data/lib/reports_kit/model_configuration.rb +23 -0
- data/lib/reports_kit/rails.rb +5 -0
- data/lib/reports_kit/report_builder.rb +76 -0
- data/lib/reports_kit/reports/data/chart_options.rb +132 -0
- data/lib/reports_kit/reports/data/generate.rb +65 -0
- data/lib/reports_kit/reports/data/one_dimension.rb +71 -0
- data/lib/reports_kit/reports/data/two_dimensions.rb +129 -0
- data/lib/reports_kit/reports/data/utils.rb +79 -0
- data/lib/reports_kit/reports/dimension.rb +78 -0
- data/lib/reports_kit/reports/filter.rb +84 -0
- data/lib/reports_kit/reports/filter_types/base.rb +47 -0
- data/lib/reports_kit/reports/filter_types/boolean.rb +30 -0
- data/lib/reports_kit/reports/filter_types/datetime.rb +27 -0
- data/lib/reports_kit/reports/filter_types/number.rb +28 -0
- data/lib/reports_kit/reports/filter_types/records.rb +26 -0
- data/lib/reports_kit/reports/filter_types/string.rb +38 -0
- data/lib/reports_kit/reports/generate_autocomplete_results.rb +55 -0
- data/lib/reports_kit/reports/inferrable_configuration.rb +113 -0
- data/lib/reports_kit/reports/measure.rb +58 -0
- data/lib/reports_kit/reports_controller.rb +15 -0
- data/lib/reports_kit/resources_controller.rb +8 -0
- data/lib/reports_kit/version.rb +3 -0
- data/reports_kit.gemspec +23 -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/repo_factory.rb +5 -0
- data/spec/factories/tag_factory.rb +4 -0
- data/spec/fixtures/generate_inputs.yml +35 -0
- data/spec/fixtures/generate_outputs.yml +208 -0
- data/spec/reports_kit/reports/data/generate_spec.rb +275 -0
- data/spec/reports_kit/reports/dimension_spec.rb +38 -0
- data/spec/reports_kit/reports/filter_spec.rb +38 -0
- data/spec/spec_helper.rb +58 -0
- data/spec/support/factory_girl.rb +5 -0
- data/spec/support/helpers.rb +13 -0
- data/spec/support/models/issue.rb +6 -0
- data/spec/support/models/issues_label.rb +4 -0
- data/spec/support/models/label.rb +5 -0
- data/spec/support/models/repo.rb +7 -0
- data/spec/support/models/tag.rb +4 -0
- data/spec/support/schema.rb +38 -0
- metadata +232 -0
data/config/routes.rb
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/gists/doc.txt
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
1. Add ReportsKit
|
|
2
|
+
|
|
3
|
+
Gemfile
|
|
4
|
+
|
|
5
|
+
source 'https://my-api-key@gems.reportskit.co' do
|
|
6
|
+
gem 'reportskit'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
2. Configure Models
|
|
11
|
+
|
|
12
|
+
Configure the filters and dimensions that can be used in your reports:
|
|
13
|
+
|
|
14
|
+
app/models/task.rb
|
|
15
|
+
|
|
16
|
+
class Task < ActiveRecord::Base
|
|
17
|
+
belongs_to :assignee
|
|
18
|
+
belongs_to :project
|
|
19
|
+
|
|
20
|
+
reportskit do
|
|
21
|
+
filter :assignee
|
|
22
|
+
filter :project
|
|
23
|
+
filter :completed_at
|
|
24
|
+
filter :is_completed, :boolean, conditions: -> { where.not(completed_at: nil) }
|
|
25
|
+
|
|
26
|
+
dimension :assignee
|
|
27
|
+
dimension :project
|
|
28
|
+
dimension :completed_at
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
3. Configure Reports
|
|
34
|
+
|
|
35
|
+
config/reportskit/reports/completed_tasks.yml
|
|
36
|
+
|
|
37
|
+
name: Completed tasks
|
|
38
|
+
measures:
|
|
39
|
+
- tasks
|
|
40
|
+
dimensions:
|
|
41
|
+
- completed_at
|
|
42
|
+
display_format: area
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
4. Add Report to a View
|
|
46
|
+
|
|
47
|
+
app/views/reports/my_view.html.haml
|
|
48
|
+
|
|
49
|
+
= render_report 'completed_tasks'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
5. Add Routes:
|
|
53
|
+
|
|
54
|
+
config/routes.rb
|
|
55
|
+
|
|
56
|
+
mount ReportsKit::Engine, at: '/reports'
|
|
57
|
+
|
|
58
|
+
That's it! You can now visit the view in step 4. to see the report that you've configured.
|
data/lib/reports_kit.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'rails/all'
|
|
2
|
+
|
|
3
|
+
directory = File.dirname(File.absolute_path(__FILE__))
|
|
4
|
+
Dir.glob("#{directory}/reports_kit/*.rb") { |file| require file }
|
|
5
|
+
Dir.glob("#{directory}/reports_kit/reports/data/*.rb") { |file| require file }
|
|
6
|
+
Dir.glob("#{directory}/reports_kit/reports/filter_types/*.rb") { |file| require file }
|
|
7
|
+
Dir.glob("#{directory}/reports_kit/reports/*.rb") { |file| require file }
|
|
8
|
+
|
|
9
|
+
module ReportsKit
|
|
10
|
+
def self.configure
|
|
11
|
+
yield(configuration)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.configuration
|
|
15
|
+
@configuration ||= Configuration.new
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module ReportsKit
|
|
2
|
+
class BaseController < ActionController::Base
|
|
3
|
+
private
|
|
4
|
+
|
|
5
|
+
def context_record
|
|
6
|
+
context_record_method = ReportsKit.configuration.context_record_method
|
|
7
|
+
return unless context_record_method
|
|
8
|
+
instance_eval(&context_record_method)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module ReportsKit
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
engine_name 'reports_kit'
|
|
4
|
+
|
|
5
|
+
initializer 'helper' do
|
|
6
|
+
ActiveSupport.on_load(:action_view) do
|
|
7
|
+
include Helper
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
initializer 'precompile', group: :all do |app|
|
|
12
|
+
if app.config.respond_to?(:assets)
|
|
13
|
+
if defined?(Sprockets) && Gem::Version.new(Sprockets::VERSION) >= Gem::Version.new('4.0.0.beta1')
|
|
14
|
+
app.config.assets.precompile += %w(reports_kit.js reports_kit.css)
|
|
15
|
+
else
|
|
16
|
+
app.config.assets.precompile << proc { |path| path.start_with?('reports_kit.') }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module ReportsKit
|
|
2
|
+
module Helper
|
|
3
|
+
def render_report(properties, &block)
|
|
4
|
+
raise ArgumentError.new('`properties` must be a Hash or String') if properties.blank?
|
|
5
|
+
if properties.is_a?(String)
|
|
6
|
+
path = Rails.root.join('config', 'reports_kit', 'reports', "#{properties}.yml")
|
|
7
|
+
properties = YAML.load_file(path)
|
|
8
|
+
end
|
|
9
|
+
builder = ReportsKit::ReportBuilder.new(properties)
|
|
10
|
+
content_tag :div, nil, class: 'reports_kit_report', data: { properties: builder.properties, path: reports_kit_path } do
|
|
11
|
+
if block_given?
|
|
12
|
+
form_tag reports_kit_path, method: 'get', class: 'reports_kit_report_form form-inline' do
|
|
13
|
+
capture(builder, &block)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module ReportsKit
|
|
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,23 @@
|
|
|
1
|
+
module ReportsKit
|
|
2
|
+
class ModelConfiguration
|
|
3
|
+
attr_accessor :dimensions, :filters, :autocomplete_scopes
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
self.dimensions = []
|
|
7
|
+
self.filters = []
|
|
8
|
+
self.autocomplete_scopes = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def dimension(key, properties)
|
|
12
|
+
dimensions << { key: key.to_s }.merge(properties).symbolize_keys
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def filter(key, type_key, properties)
|
|
16
|
+
filters << { key: key.to_s, type_key: type_key }.merge(properties).symbolize_keys
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def autocomplete_scope(*scopes)
|
|
20
|
+
self.autocomplete_scopes += scopes.map(&:to_s)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module ReportsKit
|
|
2
|
+
class ReportBuilder
|
|
3
|
+
include ActionView::Helpers
|
|
4
|
+
|
|
5
|
+
attr_accessor :properties
|
|
6
|
+
|
|
7
|
+
def initialize(properties)
|
|
8
|
+
self.properties = normalize_properties(properties)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def check_box(filter_key, options={})
|
|
12
|
+
filter = validate_filter!(filter_key)
|
|
13
|
+
checked = filter.properties[:criteria][:operator] == 'true'
|
|
14
|
+
check_box_tag(filter_key, '1', checked, options)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def date_range(filter_key, options={})
|
|
18
|
+
filter = validate_filter!(filter_key)
|
|
19
|
+
defaults = { class: 'form-control input-sm date_range_picker' }
|
|
20
|
+
options = defaults.deep_merge(options)
|
|
21
|
+
text_field_tag(filter_key, filter.properties[:criteria][:value], options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def multi_autocomplete(filter_key, options={})
|
|
25
|
+
validate_filter!(filter_key)
|
|
26
|
+
reports_kit_path = Rails.application.routes.url_helpers.reports_kit_path
|
|
27
|
+
path = "#{reports_kit_path}reports_kit/resources/measures/#{measure.key}/filters/#{filter_key}/autocomplete"
|
|
28
|
+
scope = options.delete(:scope)
|
|
29
|
+
params = {}
|
|
30
|
+
params[:scope] = scope if scope.present?
|
|
31
|
+
|
|
32
|
+
defaults = {
|
|
33
|
+
class: 'form-control input-sm select2',
|
|
34
|
+
multiple: 'multiple',
|
|
35
|
+
data: {
|
|
36
|
+
placeholder: options[:placeholder],
|
|
37
|
+
path: path,
|
|
38
|
+
params: params
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
options = defaults.deep_merge(options)
|
|
42
|
+
select_tag(filter_key, nil, options)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def string_filter(filter_key, options={})
|
|
46
|
+
filter = validate_filter!(filter_key)
|
|
47
|
+
defaults = { class: 'form-control input-sm' }
|
|
48
|
+
options = defaults.deep_merge(options)
|
|
49
|
+
text_field_tag(filter_key, filter.properties[:criteria][:value], options)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def validate_filter!(filter_key)
|
|
55
|
+
filter_key = filter_key.to_s
|
|
56
|
+
filter = filters.find { |f| f.key == filter_key }
|
|
57
|
+
raise ArgumentError.new("A filter with key '#{filter_key}' is not configured in this report") unless filter
|
|
58
|
+
filter
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def filters
|
|
62
|
+
measure.filters
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def measure
|
|
66
|
+
Reports::Measure.new(properties[:measure])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def normalize_properties(properties)
|
|
70
|
+
properties = properties.deep_symbolize_keys
|
|
71
|
+
measure = Reports::Measure.new(properties[:measure])
|
|
72
|
+
properties[:measure] = measure.properties_with_filters
|
|
73
|
+
properties
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
module ReportsKit
|
|
2
|
+
module Reports
|
|
3
|
+
module Data
|
|
4
|
+
class ChartOptions
|
|
5
|
+
DEFAULT_COLORS = %w(
|
|
6
|
+
#1f77b4
|
|
7
|
+
#aec7e8
|
|
8
|
+
#ff7f0e
|
|
9
|
+
#ffbb78
|
|
10
|
+
#2ca02c
|
|
11
|
+
#98df8a
|
|
12
|
+
#d62728
|
|
13
|
+
#ff9896
|
|
14
|
+
#9467bd
|
|
15
|
+
#c5b0d5
|
|
16
|
+
#8c564b
|
|
17
|
+
#c49c94
|
|
18
|
+
#e377c2
|
|
19
|
+
#f7b6d2
|
|
20
|
+
#7f7f7f
|
|
21
|
+
#c7c7c7
|
|
22
|
+
#bcbd22
|
|
23
|
+
#dbdb8d
|
|
24
|
+
#17becf
|
|
25
|
+
#9edae5
|
|
26
|
+
).freeze
|
|
27
|
+
DEFAULT_OPTIONS = {
|
|
28
|
+
scales: {
|
|
29
|
+
xAxes: [{
|
|
30
|
+
gridLines: {
|
|
31
|
+
display: false
|
|
32
|
+
},
|
|
33
|
+
barPercentage: 0.9,
|
|
34
|
+
categoryPercentage: 0.9
|
|
35
|
+
}],
|
|
36
|
+
yAxes: [{
|
|
37
|
+
ticks: {
|
|
38
|
+
beginAtZero: true
|
|
39
|
+
}
|
|
40
|
+
}]
|
|
41
|
+
},
|
|
42
|
+
legend: {
|
|
43
|
+
labels: {
|
|
44
|
+
usePointStyle: true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
tooltips: {
|
|
48
|
+
xPadding: 8,
|
|
49
|
+
yPadding: 7
|
|
50
|
+
}
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
attr_accessor :data, :options, :chart_options, :inferred_options, :dataset_options, :type
|
|
54
|
+
|
|
55
|
+
def initialize(data, options:, inferred_options: {})
|
|
56
|
+
self.data = data
|
|
57
|
+
self.options = options.try(:except, :options) || {}
|
|
58
|
+
self.chart_options = options.try(:[], :options) || {}
|
|
59
|
+
self.dataset_options = options.try(:[], :datasets)
|
|
60
|
+
self.type = options.try(:[], :type) || 'bar'
|
|
61
|
+
|
|
62
|
+
self.options = inferred_options.deep_merge(self.options) if inferred_options.present?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def perform
|
|
66
|
+
set_colors
|
|
67
|
+
set_chart_options
|
|
68
|
+
set_dataset_options
|
|
69
|
+
set_type
|
|
70
|
+
data
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def set_colors
|
|
76
|
+
self.data[:chart_data][:datasets] = data[:chart_data][:datasets].map.with_index do |dataset, index|
|
|
77
|
+
color = DEFAULT_COLORS[index % DEFAULT_COLORS.length]
|
|
78
|
+
dataset[:backgroundColor] = color
|
|
79
|
+
dataset[:borderColor] = color
|
|
80
|
+
dataset
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def default_options
|
|
85
|
+
@default_options ||= begin
|
|
86
|
+
default_options = DEFAULT_OPTIONS.deep_dup
|
|
87
|
+
|
|
88
|
+
x_axis_label = options[:x_axis_label]
|
|
89
|
+
if x_axis_label
|
|
90
|
+
default_options[:scales] ||= {}
|
|
91
|
+
default_options[:scales][:xAxes] ||= []
|
|
92
|
+
default_options[:scales][:xAxes][0] ||= {}
|
|
93
|
+
default_options[:scales][:xAxes][0][:scaleLabel] ||= {}
|
|
94
|
+
default_options[:scales][:xAxes][0][:scaleLabel][:display] ||= true
|
|
95
|
+
default_options[:scales][:xAxes][0][:scaleLabel][:labelString] ||= x_axis_label
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
y_axis_label = options[:y_axis_label]
|
|
99
|
+
if y_axis_label
|
|
100
|
+
default_options[:scales] ||= {}
|
|
101
|
+
default_options[:scales][:yAxes] ||= []
|
|
102
|
+
default_options[:scales][:yAxes][0] ||= {}
|
|
103
|
+
default_options[:scales][:yAxes][0][:scaleLabel] ||= {}
|
|
104
|
+
default_options[:scales][:yAxes][0][:scaleLabel][:display] ||= true
|
|
105
|
+
default_options[:scales][:yAxes][0][:scaleLabel][:labelString] ||= y_axis_label
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
default_options
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def set_chart_options
|
|
113
|
+
merged_options = default_options
|
|
114
|
+
merged_options = merged_options.deep_merge(chart_options) if chart_options
|
|
115
|
+
self.data[:chart_data][:options] = merged_options
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def set_dataset_options
|
|
119
|
+
return if self.data[:chart_data][:datasets].blank? || dataset_options.blank?
|
|
120
|
+
self.data[:chart_data][:datasets] = self.data[:chart_data][:datasets].map do |dataset|
|
|
121
|
+
dataset.merge(dataset_options)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def set_type
|
|
126
|
+
return if type.blank?
|
|
127
|
+
self.data[:type] = type
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|