reports_kit 0.0.1

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