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.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.rubocop.yml +83 -0
  4. data/Gemfile +3 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.md +468 -0
  7. data/Rakefile +2 -0
  8. data/app/assets/javascripts/reports_kit/application.js +14 -0
  9. data/app/assets/javascripts/reports_kit/lib/_init.js +8 -0
  10. data/app/assets/javascripts/reports_kit/lib/chart.js +39 -0
  11. data/app/assets/javascripts/reports_kit/lib/report.js +119 -0
  12. data/app/assets/javascripts/reports_kit/vendor/chart.js +12269 -0
  13. data/app/assets/javascripts/reports_kit/vendor/daterangepicker.js +1627 -0
  14. data/app/assets/javascripts/reports_kit/vendor/moment.js +4040 -0
  15. data/app/assets/javascripts/reports_kit/vendor/select2.full.js +6436 -0
  16. data/app/assets/stylesheets/reports_kit/application.css.scss +3 -0
  17. data/app/assets/stylesheets/reports_kit/reports.css.sass +7 -0
  18. data/app/assets/stylesheets/reports_kit/select2_overrides.css.sass +7 -0
  19. data/app/assets/stylesheets/reports_kit/vendor/daterangepicker.css +269 -0
  20. data/app/assets/stylesheets/reports_kit/vendor/select2-bootstrap.css +721 -0
  21. data/app/assets/stylesheets/reports_kit/vendor/select2.css +484 -0
  22. data/config/routes.rb +10 -0
  23. data/docs/images/chart_options.png +0 -0
  24. data/docs/images/dashed_line.png +0 -0
  25. data/docs/images/flights_by_carrier.png +0 -0
  26. data/docs/images/flights_by_carrier_and_flight_at.png +0 -0
  27. data/docs/images/flights_by_delay.png +0 -0
  28. data/docs/images/flights_by_flight_at.png +0 -0
  29. data/docs/images/flights_by_hours_delayed.png +0 -0
  30. data/docs/images/flights_with_check_box.png +0 -0
  31. data/docs/images/flights_with_configured_boolean.png +0 -0
  32. data/docs/images/flights_with_configured_datetime.png +0 -0
  33. data/docs/images/flights_with_configured_number.png +0 -0
  34. data/docs/images/flights_with_configured_string.png +0 -0
  35. data/docs/images/flights_with_date_range.png +0 -0
  36. data/docs/images/flights_with_filters.png +0 -0
  37. data/docs/images/flights_with_multi_autocomplete.png +0 -0
  38. data/docs/images/flights_with_string_filter.png +0 -0
  39. data/docs/images/horizontal_bar.png +0 -0
  40. data/docs/images/legend_right.png +0 -0
  41. data/docs/images/users_by_created_at.png +0 -0
  42. data/gists/doc.txt +58 -0
  43. data/lib/reports_kit.rb +17 -0
  44. data/lib/reports_kit/base_controller.rb +11 -0
  45. data/lib/reports_kit/configuration.rb +10 -0
  46. data/lib/reports_kit/engine.rb +21 -0
  47. data/lib/reports_kit/helper.rb +19 -0
  48. data/lib/reports_kit/model.rb +16 -0
  49. data/lib/reports_kit/model_configuration.rb +23 -0
  50. data/lib/reports_kit/rails.rb +5 -0
  51. data/lib/reports_kit/report_builder.rb +76 -0
  52. data/lib/reports_kit/reports/data/chart_options.rb +132 -0
  53. data/lib/reports_kit/reports/data/generate.rb +65 -0
  54. data/lib/reports_kit/reports/data/one_dimension.rb +71 -0
  55. data/lib/reports_kit/reports/data/two_dimensions.rb +129 -0
  56. data/lib/reports_kit/reports/data/utils.rb +79 -0
  57. data/lib/reports_kit/reports/dimension.rb +78 -0
  58. data/lib/reports_kit/reports/filter.rb +84 -0
  59. data/lib/reports_kit/reports/filter_types/base.rb +47 -0
  60. data/lib/reports_kit/reports/filter_types/boolean.rb +30 -0
  61. data/lib/reports_kit/reports/filter_types/datetime.rb +27 -0
  62. data/lib/reports_kit/reports/filter_types/number.rb +28 -0
  63. data/lib/reports_kit/reports/filter_types/records.rb +26 -0
  64. data/lib/reports_kit/reports/filter_types/string.rb +38 -0
  65. data/lib/reports_kit/reports/generate_autocomplete_results.rb +55 -0
  66. data/lib/reports_kit/reports/inferrable_configuration.rb +113 -0
  67. data/lib/reports_kit/reports/measure.rb +58 -0
  68. data/lib/reports_kit/reports_controller.rb +15 -0
  69. data/lib/reports_kit/resources_controller.rb +8 -0
  70. data/lib/reports_kit/version.rb +3 -0
  71. data/reports_kit.gemspec +23 -0
  72. data/spec/factories/issue_factory.rb +4 -0
  73. data/spec/factories/issues_label_factory.rb +4 -0
  74. data/spec/factories/label_factory.rb +4 -0
  75. data/spec/factories/repo_factory.rb +5 -0
  76. data/spec/factories/tag_factory.rb +4 -0
  77. data/spec/fixtures/generate_inputs.yml +35 -0
  78. data/spec/fixtures/generate_outputs.yml +208 -0
  79. data/spec/reports_kit/reports/data/generate_spec.rb +275 -0
  80. data/spec/reports_kit/reports/dimension_spec.rb +38 -0
  81. data/spec/reports_kit/reports/filter_spec.rb +38 -0
  82. data/spec/spec_helper.rb +58 -0
  83. data/spec/support/factory_girl.rb +5 -0
  84. data/spec/support/helpers.rb +13 -0
  85. data/spec/support/models/issue.rb +6 -0
  86. data/spec/support/models/issues_label.rb +4 -0
  87. data/spec/support/models/label.rb +5 -0
  88. data/spec/support/models/repo.rb +7 -0
  89. data/spec/support/models/tag.rb +4 -0
  90. data/spec/support/schema.rb +38 -0
  91. metadata +232 -0
@@ -0,0 +1,47 @@
1
+ module ReportsKit
2
+ module Reports
3
+ module FilterTypes
4
+ class Base
5
+ attr_accessor :settings, :properties
6
+
7
+ def initialize(settings, properties)
8
+ self.settings = settings || {}
9
+ self.properties = properties
10
+ end
11
+
12
+ def apply_filter(records)
13
+ return records unless valid?
14
+ records = records.joins(joins) if joins.present?
15
+ return records if value.blank? && !is_a?(FilterTypes::Boolean)
16
+ apply_conditions(records)
17
+ end
18
+
19
+ def default_criteria
20
+ self.class::DEFAULT_CRITERIA
21
+ end
22
+
23
+ private
24
+
25
+ def apply_conditions(_records)
26
+ raise NotImplementedError
27
+ end
28
+
29
+ def criteria
30
+ properties[:criteria]
31
+ end
32
+
33
+ def value
34
+ criteria[:value]
35
+ end
36
+
37
+ def joins
38
+ settings[:joins]
39
+ end
40
+
41
+ def column
42
+ settings[:column] || properties[:key]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,30 @@
1
+ module ReportsKit
2
+ module Reports
3
+ module FilterTypes
4
+ class Boolean < Base
5
+ DEFAULT_CRITERIA = {
6
+ operator: nil
7
+ }
8
+
9
+ def apply_conditions(records)
10
+ case criteria[:operator]
11
+ when true, 'true'
12
+ records.where("(#{column}) = true")
13
+ when false, 'false'
14
+ records.where("(#{column}) != true")
15
+ else
16
+ raise ArgumentError.new("Unsupported operator: '#{criteria[:operator]}'")
17
+ end
18
+ end
19
+
20
+ def valid?
21
+ criteria[:operator].present?
22
+ end
23
+
24
+ def column
25
+ settings[:conditions] || properties[:key]
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ module ReportsKit
2
+ module Reports
3
+ module FilterTypes
4
+ class Datetime < Base
5
+ DEFAULT_CRITERIA = {
6
+ operator: 'between'
7
+ }
8
+
9
+ def apply_conditions(records)
10
+ case criteria[:operator]
11
+ when 'between'
12
+ start_string, end_string = value.split(' - ')
13
+ start_at = Date.parse(start_string)
14
+ end_at = Date.parse(end_string)
15
+ records.where("#{column} IS NOT NULL").where("#{column} BETWEEN ? AND ?", start_at, end_at)
16
+ else
17
+ raise ArgumentError.new("Unsupported operator: '#{criteria[:operator]}'")
18
+ end
19
+ end
20
+
21
+ def valid?
22
+ value.present?
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ module ReportsKit
2
+ module Reports
3
+ module FilterTypes
4
+ class Number < Base
5
+ def apply_conditions(records)
6
+ case criteria[:operator]
7
+ when '>'
8
+ records.where("#{column} > #{value.to_i}")
9
+ when '>='
10
+ records.where("#{column} >= #{value.to_i}")
11
+ when '<'
12
+ records.where("#{column} < #{value.to_i}")
13
+ when '<='
14
+ records.where("#{column} <= #{value.to_i}")
15
+ when '='
16
+ records.where("#{column} = #{value.to_i}")
17
+ else
18
+ raise ArgumentError.new("Unsupported operator: '#{criteria[:operator]}'")
19
+ end
20
+ end
21
+
22
+ def valid?
23
+ value.present?
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ module ReportsKit
2
+ module Reports
3
+ module FilterTypes
4
+ class Records < Base
5
+ DEFAULT_CRITERIA = {
6
+ operator: 'include'
7
+ }
8
+
9
+ def apply_conditions(records)
10
+ case criteria[:operator]
11
+ when 'include'
12
+ records.where(column => value)
13
+ when 'does_not_include'
14
+ records.where.not(column => value)
15
+ else
16
+ raise ArgumentError.new("Unsupported operator: '#{criteria[:operator]}'")
17
+ end
18
+ end
19
+
20
+ def valid?
21
+ value.present?
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,38 @@
1
+ module ReportsKit
2
+ module Reports
3
+ module FilterTypes
4
+ class String < Base
5
+ DEFAULT_CRITERIA = {
6
+ operator: 'contains'
7
+ }
8
+
9
+ def apply_conditions(records)
10
+ case criteria[:operator]
11
+ when 'equals'
12
+ records.where("#{column} = ?", value)
13
+ when 'contains'
14
+ records.where("#{column} ILIKE ?", "%#{value}%")
15
+ when 'starts_with'
16
+ records.where("#{column} ILIKE ?", "#{value}%")
17
+ when 'ends_with'
18
+ records.where("#{column} ILIKE ?", "%#{value}")
19
+ when 'does_not_equal'
20
+ records.where("#{column} != ?", value)
21
+ when 'does_not_contain'
22
+ records.where("#{column} NOT ILIKE ?", "%#{value}%")
23
+ when 'does_not_start_with'
24
+ records.where("#{column} NOT ILIKE ?", "#{value}%")
25
+ when 'does_not_end_with'
26
+ records.where("#{column} NOT ILIKE ?", "%#{value}")
27
+ else
28
+ raise ArgumentError.new("Unsupported operator: '#{criteria[:operator]}'")
29
+ end
30
+ end
31
+
32
+ def valid?
33
+ value.present?
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,55 @@
1
+ module ReportsKit
2
+ module Reports
3
+ class GenerateAutocompleteResults
4
+ attr_accessor :params, :measure_key, :filter_key, :context_record
5
+
6
+ def initialize(params, context_record: nil)
7
+ self.params = params
8
+ self.measure_key = params[:measure_key]
9
+ self.filter_key = params[:filter_key]
10
+ self.context_record = context_record
11
+ end
12
+
13
+ def perform
14
+ raise ArgumentError.new("Could not find a model for filter_key: '#{filter_key}'") unless model
15
+ results = model
16
+ results = results.public_send(scope) if scope
17
+ results = results.limit(10_000)
18
+ results = results.map { |result| { id: result.id, text: result.to_s } }
19
+ results = results.sort_by { |result| result[:text].downcase }
20
+ results = filter_results(results)
21
+ results
22
+ end
23
+
24
+ private
25
+
26
+ def filter_results(results)
27
+ query = params[:q].try(:downcase)
28
+ if query.present?
29
+ results = results.to_a.select { |r| r[:text].downcase.include?(query) }
30
+ end
31
+ results
32
+ end
33
+
34
+ def model
35
+ @model ||= begin
36
+ measure = Measure.new(measure_key, context_record: context_record)
37
+ filter = Filter.new(filter_key, measure: measure)
38
+ filter.instance_class
39
+ end
40
+ end
41
+
42
+ def scope
43
+ @scope ||= begin
44
+ scope = params[:scope]
45
+ return unless scope.present?
46
+ return unless model.try(:reports_kit_configuration) && model.reports_kit_configuration.autocomplete_scopes.present?
47
+ unless model.reports_kit_configuration.autocomplete_scopes.include?(scope)
48
+ raise ArgumentError.new("Unallowed scope '#{scope}' for model #{model.name}")
49
+ end
50
+ scope
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,113 @@
1
+ module ReportsKit
2
+ module Reports
3
+ class InferrableConfiguration
4
+ SUPPORTED_COLUMN_TYPES = [
5
+ :boolean,
6
+ :datetime,
7
+ :integer,
8
+ :string
9
+ ]
10
+
11
+ attr_accessor :inferrable, :inferrable_type
12
+
13
+ delegate :key, :measure, to: :inferrable
14
+
15
+ def initialize(inferrable, inferrable_type)
16
+ self.inferrable = inferrable
17
+ self.inferrable_type = inferrable_type
18
+ end
19
+
20
+ def configuration_strategy
21
+ if settings_from_model.present?
22
+ :model
23
+ elsif reflection
24
+ :association
25
+ elsif column_type
26
+ :column
27
+ else
28
+ inferrable_type_string = inferrable_type.to_s.singularize
29
+ raise ArgumentError.new("No configuration found on the #{model_class} model for #{inferrable_type_string} with key: '#{key}'")
30
+ end
31
+ end
32
+
33
+ def configured_by_association?
34
+ configuration_strategy == :association
35
+ end
36
+
37
+ def configured_by_column?
38
+ configuration_strategy == :column
39
+ end
40
+
41
+ def configured_by_model?
42
+ configuration_strategy == :model
43
+ end
44
+
45
+ def configured_by_time?
46
+ column_type == :datetime
47
+ end
48
+
49
+ def settings_from_model
50
+ return {} if model_configuration.blank?
51
+ return {} if model_configuration.public_send(inferrable_type).blank?
52
+ config_hash = model_configuration.public_send(inferrable_type).find do |hash|
53
+ hash[:key] == key
54
+ end
55
+ config_hash || {}
56
+ end
57
+
58
+ def reflection
59
+ model_class.reflect_on_association(key.to_sym)
60
+ end
61
+
62
+ def instance_class
63
+ return reflection.class_name.constantize if reflection
64
+ nil
65
+ end
66
+
67
+ def column
68
+ return unless inferred_settings
69
+ inferred_settings[:column]
70
+ end
71
+
72
+ def inferred_settings
73
+ return { column: "#{model_class.table_name}.#{key}" } if configured_by_column?
74
+ if configured_by_association?
75
+ return { column: "#{model_class.table_name}.#{reflection.foreign_key}" } if reflection.macro == :belongs_to
76
+ return inferred_settings_from_has_many if inferred_settings_from_has_many
77
+ end
78
+ {}
79
+ end
80
+
81
+ def inferred_settings_from_has_many
82
+ return unless reflection.macro == :has_many
83
+ through_reflection = reflection.through_reflection
84
+ if through_reflection
85
+ {
86
+ joins: through_reflection.name,
87
+ column: "#{through_reflection.table_name}.#{reflection.source_reflection.foreign_key}"
88
+ }
89
+ else
90
+ {
91
+ joins: reflection.name,
92
+ column: "#{reflection.klass.table_name}.#{reflection.klass.primary_key}"
93
+ }
94
+ end
95
+ end
96
+
97
+ def column_type
98
+ column_type = model_class.columns_hash[key.to_s].try(:type)
99
+ return column_type if SUPPORTED_COLUMN_TYPES.include?(column_type)
100
+ end
101
+
102
+ def model_configuration
103
+ return unless model_class && model_class.respond_to?(:reports_kit_configuration)
104
+ model_class.reports_kit_configuration
105
+ end
106
+
107
+ def model_class
108
+ return unless measure
109
+ measure.model_class
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,58 @@
1
+ module ReportsKit
2
+ module Reports
3
+ class Measure
4
+ attr_accessor :properties, :filters, :context_record
5
+
6
+ def initialize(properties, context_record: nil)
7
+ properties = { key: properties } if properties.is_a?(String)
8
+ raise ArgumentError.new("Measure properties must be a String or Hash, not a #{properties.class.name}: #{properties.inspect}") unless properties.is_a?(Hash)
9
+ properties = properties.deep_symbolize_keys
10
+ filter_hashes = properties.delete(:filters) || []
11
+ filter_hashes = filter_hashes.values if filter_hashes.is_a?(Hash) && filter_hashes.key?(:'0')
12
+
13
+ self.properties = properties
14
+ self.filters = filter_hashes.map { |filter_hash| Filter.new(filter_hash, measure: self) }
15
+ self.context_record = context_record
16
+ end
17
+
18
+ def key
19
+ properties[:key]
20
+ end
21
+
22
+ def label
23
+ key.pluralize.titleize
24
+ end
25
+
26
+ def aggregate_function
27
+ :count
28
+ end
29
+
30
+ def conditions
31
+ nil
32
+ end
33
+
34
+ def base_relation
35
+ return context_record.public_send(key.pluralize) if context_record
36
+ model_class
37
+ end
38
+
39
+ def model_class
40
+ key.camelize.constantize
41
+ end
42
+
43
+ def filtered_relation
44
+ relation = base_relation
45
+ filters.each do |filter|
46
+ relation = filter.apply(relation)
47
+ end
48
+ relation
49
+ end
50
+
51
+ def properties_with_filters
52
+ all_properties = properties
53
+ all_properties[:filters] = filters.map(&:properties)
54
+ all_properties
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,15 @@
1
+ module ReportsKit
2
+ class ReportsController < ReportsKit::BaseController
3
+ def index
4
+ # RubyProf.start
5
+ properties = ActiveSupport::JSON.decode(params[:properties])
6
+ report_data = Reports::Data::Generate.new(properties, context_record: context_record).perform
7
+ # result = RubyProf.stop
8
+ # printer = RubyProf::CallStackPrinter.new(result)
9
+ # File.open(Rails.root.join('tmp', 'out.html'), 'w') do |file|
10
+ # printer.print(file, :min_percent => 2)
11
+ # end
12
+ render json: { data: report_data }
13
+ end
14
+ end
15
+ end