compendium 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +15 -0
  2. data/README.md +70 -4
  3. data/Rakefile +4 -0
  4. data/app/assets/stylesheets/compendium/_metrics.css.scss +92 -0
  5. data/app/assets/stylesheets/compendium/options.css.scss +41 -0
  6. data/app/assets/stylesheets/compendium/report.css.scss +22 -0
  7. data/app/classes/compendium/presenters/base.rb +30 -0
  8. data/app/classes/compendium/presenters/chart.rb +31 -0
  9. data/app/classes/compendium/presenters/metric.rb +19 -0
  10. data/app/classes/compendium/presenters/option.rb +97 -0
  11. data/app/classes/compendium/presenters/query.rb +23 -0
  12. data/app/classes/compendium/presenters/settings/query.rb +22 -0
  13. data/app/classes/compendium/presenters/settings/table.rb +23 -0
  14. data/app/classes/compendium/presenters/table.rb +81 -0
  15. data/app/controllers/compendium/reports_controller.rb +52 -0
  16. data/app/helpers/compendium/reports_helper.rb +21 -0
  17. data/app/views/compendium/reports/run.haml +1 -0
  18. data/app/views/compendium/reports/setup.haml +14 -0
  19. data/compendium.gemspec +7 -1
  20. data/config/initializers/rails/active_record/connection_adapters/quoting.rb +14 -0
  21. data/config/initializers/ruby/numeric.rb +26 -0
  22. data/config/locales/en.yml +5 -0
  23. data/lib/compendium/abstract_chart_provider.rb +30 -0
  24. data/lib/compendium/chart_provider/amcharts.rb +20 -0
  25. data/lib/compendium/context_wrapper.rb +27 -0
  26. data/lib/compendium/dsl.rb +79 -0
  27. data/lib/compendium/engine/mount.rb +13 -0
  28. data/lib/compendium/engine.rb +8 -0
  29. data/lib/compendium/metric.rb +29 -0
  30. data/lib/compendium/open_hash.rb +68 -0
  31. data/lib/compendium/option.rb +37 -0
  32. data/lib/compendium/param_types.rb +91 -0
  33. data/lib/compendium/params.rb +40 -0
  34. data/lib/compendium/query.rb +94 -0
  35. data/lib/compendium/report.rb +56 -0
  36. data/lib/compendium/result_set.rb +24 -0
  37. data/lib/compendium/version.rb +1 -1
  38. data/lib/compendium.rb +46 -1
  39. data/spec/context_wrapper_spec.rb +71 -0
  40. data/spec/dsl_spec.rb +90 -0
  41. data/spec/metric_spec.rb +84 -0
  42. data/spec/option_spec.rb +12 -0
  43. data/spec/param_types_spec.rb +147 -0
  44. data/spec/params_spec.rb +28 -0
  45. data/spec/presenters/base_spec.rb +20 -0
  46. data/spec/presenters/option_spec.rb +49 -0
  47. data/spec/query_spec.rb +33 -0
  48. data/spec/report_spec.rb +93 -0
  49. data/spec/spec_helper.rb +1 -0
  50. metadata +135 -14
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MDMzZWY3N2Y4MWU5MGUxODg2YzFjZWY2ZDgzNzE0ZTU0MmNkN2IyYw==
5
+ data.tar.gz: !binary |-
6
+ ZDBjNTEwN2NjNGNlZWIxMzhiMWQ5MGRmMzg0NmY4NGQyNTlhYTAwNA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ MDQ1M2YwZDVjMWEyN2M2NjM4ZDYxOGY4ZTEwOTQ0M2Y0MWIzM2UxNDM2MGVm
10
+ YzIwMDFkYzM1ZjliNDUxMjBjNzQxNDkyMTIzYzlhNWQyNjViYTMwYmRhYmUy
11
+ ODBiODRhZjRkOGQ0MWVjNDdiNjE4MmE3MjM2OGNkNDZlZTE0NDk=
12
+ data.tar.gz: !binary |-
13
+ NjIzZGIwNWY1NTU3MzViOTU5Njc3MzFiNTdiYzdhZWViYjIwOGVlYzZmMDhm
14
+ NWI5NzkwNDJjYTUzYjIwNDg4ZjZkZDJhYzE5MDE0ZGQyM2U0Y2JlMGIzNThk
15
+ ODY0NDFkY2NhMDg1MWZkYmU3MjlmNjdhMzIxZWUzYWFjNjIyMzU=
data/README.md CHANGED
@@ -2,6 +2,72 @@
2
2
 
3
3
  Ruby on Rails framework for making reporting easy.
4
4
 
5
+ ## Usage
6
+
7
+ Compendium is a reporting framework for Rails which makes it easy to create and render reports (with charts and tables).
8
+
9
+ A Compendium report is a subclass of `Compendium::Report`. Reports can be defined using the simple DSL:
10
+
11
+ class MyReport < Compendium::Report
12
+ # Options define which parameters your report will accept when being set up.
13
+ # An option is defined with a name, a type, and some settings (ie. default value, choices for radio buttons and
14
+ # dropdowns, etc.)
15
+ option :starting_on, :date, default: -> { Date.today - 1.month }
16
+ option :ending_on, :date, default: -> { Date.today }
17
+ option :currency, :radio, choices: [:USD, :CAD, :GBP]
18
+
19
+ # By default, queries are converted to SQL and executed instead of returning AR models
20
+ # The query definition block gets the report's current parameters
21
+ # totals: true means that the last row returned should be interpretted as a row of totals
22
+ query :deliveries, totals: true do |params|
23
+ Items.where(delivered: true, purchased_at: (params[:starting_on]..params[:ending_on]))
24
+ end
25
+
26
+ # Define a query which collects data by using AR directly
27
+ query :on_hand_inventory, collect: :active_record do |params|
28
+ Items.where(in_stock: true)
29
+ end
30
+
31
+ # Define a query that works on another query's result set
32
+ # Note: chart and data are aliases for query
33
+ chart :deliveries_over_time, through: :deliveries do |results|
34
+ results.group_by(&:purchased_at)
35
+ end
36
+
37
+ # Queries can also be used to drive metrics
38
+ metric :shipping_time, -> results { results.last['shipping_time'] }, through: :deliveries
39
+ end
40
+
41
+ Reports can then also be simply instantiated (which is done automatically if using the supplied
42
+ `Compendium::ReportsController`):
43
+
44
+ report = MyReport.new(starting_on: '2013-06-01')
45
+ report.run(self) # The parameter is the context to run the report in; usually this should be
46
+ # a controller context so that methods like current_user can be used
47
+
48
+ Compendium also comes with a variety of different presenters, for rendering the setup page, and displaying charts
49
+ (`report.render_chart`), tables (`report.render_table`) and metrics for your report. Charting is delegated through a
50
+ `ChartProvider` to a charting gem (amcharts.rb is currently supported).
51
+
52
+ ### Tying into your Rails application
53
+
54
+ Compendium has a `Rails::Engine`, which adds a default controller and some views. If desired, the controller can be
55
+ subclassed so that filters and the like can be added. The controller (which extends `ApplicationController`
56
+ automatically) has two actions: `setup` (collect options for the report) and `run` (execute and render the report),
57
+ with accompanying views. The `setup` view can be included inside your own view using the `render_report_setup`
58
+ method (*NOTE:* you have to pass `local_assigns` into it if you want locals to be passed along).
59
+
60
+ Routes are not automatically added to your application. In order to do so, you can use the `mount_compendium` helper
61
+ within your `config/routes.rb` file
62
+
63
+ mount_compendium at: '/report', controller: 'reports' # controller defaults to compendium/reports
64
+
65
+ ### Interaction with other gems
66
+ * If [accessible_tooltip](https://github.com/dvandersluis/accessible_tooltip) is present, option notes will be rendered
67
+ in a tooltip rather than as straight text.
68
+ * [AmCharts.rb](https://github.com/dvandersluis/amcharts.rb) is currently the only chart provider (please create a pull
69
+ request if you'd like to create another one...)
70
+
5
71
  ## Installation
6
72
 
7
73
  Add this line to your application's Gemfile:
@@ -16,10 +82,6 @@ Or install it yourself as:
16
82
 
17
83
  $ gem install compendium
18
84
 
19
- ## Usage
20
-
21
- TODO: Write usage instructions here
22
-
23
85
  ## Contributing
24
86
 
25
87
  1. Fork it
@@ -27,3 +89,7 @@ TODO: Write usage instructions here
27
89
  3. Commit your changes (`git commit -am 'Add some feature'`)
28
90
  4. Push to the branch (`git push origin my-new-feature`)
29
91
  5. Create new Pull Request
92
+
93
+ ## Acknowledgments
94
+
95
+ * Special thanks to [TalentNest](http://github.com/talentnest), who sponsored this gem's development.
data/Rakefile CHANGED
@@ -1 +1,5 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
@@ -0,0 +1,92 @@
1
+ @import "compass/css3/border-radius";
2
+
3
+ div.metrics
4
+ {
5
+ div.metric
6
+ {
7
+ float: left;
8
+ margin: 10px;
9
+ padding: 5px;
10
+ border: 2px solid #575757;
11
+ width: 150px;
12
+ height: 150px;
13
+ font-size: 12px;
14
+ text-align: center;
15
+ color: #575757;
16
+ @include border-radius(10px);
17
+
18
+ .metric-label
19
+ {
20
+ height: 30%;
21
+ font-size: 150%;
22
+ padding-top: 10px;
23
+ padding-bottom: 5px;
24
+ font-weight: bold;
25
+ }
26
+
27
+ .metric-data
28
+ {
29
+ height: 50%;
30
+ position: relative;
31
+ font-size: 600%;
32
+ font-weight: bold;
33
+ color: black;
34
+
35
+ .metric-data-inner
36
+ {
37
+ display: inline;
38
+ position: relative;
39
+ line-height: 0;
40
+ top: 50%;
41
+ margin: auto;
42
+ padding: 0;
43
+ }
44
+ }
45
+
46
+ .metric-label-small
47
+ {
48
+ height: 20%;
49
+ font_size: 90%;
50
+ }
51
+
52
+ &.small
53
+ {
54
+ width: 115px;
55
+ height: 115px;
56
+ font-size: 10px;
57
+
58
+ .metric-label
59
+ {
60
+ padding-top: 0px;
61
+ }
62
+
63
+ .metric-label-small
64
+ {
65
+ font-size: 100%;
66
+ }
67
+ }
68
+
69
+ &.big
70
+ {
71
+ border-width: 3px;
72
+ width: 280px;
73
+ height: 280px;
74
+ font-size: 22px;
75
+
76
+ .metric-label
77
+ {
78
+ padding-top: 25px;
79
+ }
80
+
81
+ .metric-label-small
82
+ {
83
+ margin-top: -25px;
84
+ }
85
+
86
+ .metric-data
87
+ {
88
+ top: -10px;
89
+ }
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,41 @@
1
+ .option
2
+ {
3
+ .option-label
4
+ {
5
+ margin: 0px 0px 0px 0px;
6
+ font-family: "Arial";
7
+ font-size: 14px;
8
+ font-weight: bold;
9
+ color: #575757;
10
+ }
11
+
12
+ .option-note
13
+ {
14
+ font-style: italic;
15
+ color: #575757;
16
+ }
17
+
18
+ .option-element-group
19
+ {
20
+ margin-top: 3px;
21
+ }
22
+
23
+ .option-radio
24
+ {
25
+ input[type=radio]
26
+ {
27
+ margin: 2px 6px 2px 0;
28
+
29
+ & + label
30
+ {
31
+ position: relative;
32
+ top: 1px;
33
+ }
34
+ }
35
+ }
36
+
37
+ & + &, & + input[type=submit]
38
+ {
39
+ margin-top: 15px;
40
+ }
41
+ }
@@ -0,0 +1,22 @@
1
+ .report
2
+ {
3
+ @import 'metrics';
4
+
5
+ .title
6
+ {
7
+ font-size: 200%;
8
+ font-weight: bold;
9
+ }
10
+
11
+ .subtitle
12
+ {
13
+ font-size: 150%;
14
+ font-weight: bold;
15
+ }
16
+
17
+ .setting, .option
18
+ {
19
+ font-weight: bold;
20
+ color: #575757;
21
+ }
22
+ }
@@ -0,0 +1,30 @@
1
+ module Compendium::Presenters
2
+ class Base
3
+ def initialize(template, object)
4
+ @object = object
5
+ @template = template
6
+ end
7
+
8
+ def to_s
9
+ "#<#{self.class.name}:0x00#{'%x' % (object_id << 1)}>"
10
+ end
11
+
12
+ private
13
+
14
+ def self.presents(name)
15
+ define_method(name) do
16
+ @object
17
+ end
18
+ end
19
+
20
+ def method_missing(*args, &block)
21
+ return @template.send(*args, &block) if @template.respond_to?(args.first)
22
+ super
23
+ end
24
+
25
+ def respond_to_missing?(*args)
26
+ return true if @template.respond_to?(*args)
27
+ super
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ module Compendium::Presenters
2
+ class Chart < Query
3
+ attr_reader :data, :chart_provider
4
+
5
+ def initialize(template, object, type, container = nil, &setup)
6
+ super(template, object)
7
+
8
+ @data = results.records
9
+ @data = @data[0...-1] if query.options[:totals]
10
+
11
+ @container = container || query.name
12
+
13
+ initialize_chart_provider(type, &setup)
14
+ end
15
+
16
+ def render
17
+ chart_provider.render(@template, @container)
18
+ end
19
+
20
+ private
21
+
22
+ def provider
23
+ provider = Compendium.config.chart_provider
24
+ provider.is_a?(Class) ? provider : Compendium::ChartProvider.const_get(provider)
25
+ end
26
+
27
+ def initialize_chart_provider(type, &setup)
28
+ @chart_provider = provider.new(type, @data, &setup)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,19 @@
1
+ module Compendium::Presenters
2
+ class Metric < Base
3
+ presents :metric
4
+
5
+ delegate :name, :query, :ran?, to: :metric
6
+
7
+ def label
8
+ t("#{query}.#{name}")
9
+ end
10
+
11
+ def result(number_format = '%0.1f', display_nil_as = :na)
12
+ if metric.result
13
+ sprintf(number_format, metric.result)
14
+ else
15
+ t(display_nil_as)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,97 @@
1
+ module Compendium::Presenters
2
+ class Option < Base
3
+ MISSING_CHOICES_ERROR = "choices must be specified"
4
+
5
+ presents :option
6
+
7
+ def name
8
+ t(option.name)
9
+ end
10
+
11
+ def label(form)
12
+ label = case option.type.to_sym
13
+ when :boolean, :radio
14
+ name
15
+
16
+ else
17
+ form.label option.name, name
18
+ end
19
+
20
+ out = ActiveSupport::SafeBuffer.new
21
+ out << content_tag(:span, label, class: 'option-label')
22
+
23
+ if option.note?
24
+ note = t(option.note == true ? :"#{option.name}_note" : option.note)
25
+
26
+ if defined?(AccessibleTooltip)
27
+ return accessible_tooltip(:help, label: out, title: t("#{option.name}_note_title", default: '')) { note }
28
+ else
29
+ out << content_tag(:div, note, class: 'option-note')
30
+ end
31
+ end
32
+
33
+ out
34
+ end
35
+
36
+ def note
37
+ if option.note?
38
+ key = option.note === true ? :"#{option.name}_note" : option.note
39
+ content_tag(:div, t(key), class: 'option-note')
40
+ end
41
+ end
42
+
43
+ def input(ctx, form)
44
+ out = ActiveSupport::SafeBuffer.new
45
+
46
+ case option.type.to_sym
47
+ when :date
48
+ out << date_field(form)
49
+
50
+ when :dropdown
51
+ raise ArgumentError, MISSING_CHOICES_ERROR unless option.choices?
52
+
53
+ options = option.choices
54
+ options = ctx.instance_exec(&options) if options.respond_to?(:call)
55
+ out << dropdown(form, options)
56
+
57
+ when :boolean, :radio
58
+ choices = if option.radio?
59
+ raise ArgumentError, MISSING_CHOICES_ERROR unless option.choices?
60
+ option.choices
61
+ else
62
+ %w(true false)
63
+ end
64
+
65
+ choices.each.with_index { |choice, index| out << radio_button(form, choice, index) }
66
+ end
67
+
68
+ out
69
+ end
70
+
71
+ private
72
+
73
+ def date_field(form, include_time = false)
74
+ content_tag('div', class: 'option-date') do
75
+ if defined?(CalendarDateSelect)
76
+ form.calendar_date_select option.name, time: include_time, popup: 'force'
77
+ else
78
+ form.text_field option.name
79
+ end
80
+ end
81
+ end
82
+
83
+ def dropdown(form, choices = {})
84
+ content_tag('div', class: 'option-dropdown') do
85
+ form.select option.name, choices
86
+ end
87
+ end
88
+
89
+ def radio_button(form, label, value)
90
+ content_tag('div', class: 'option-radio') do
91
+ div_content = ActiveSupport::SafeBuffer.new
92
+ div_content << form.radio_button(option.name, value)
93
+ div_content << form.label(option.name, t(label), value: value)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,23 @@
1
+ module Compendium::Presenters
2
+ class Query < Base
3
+ presents :query
4
+
5
+ def initialize(template, object)
6
+ super(template, object)
7
+ end
8
+
9
+ def render
10
+ raise NotImplementedError
11
+ end
12
+
13
+ private
14
+
15
+ def results
16
+ query.results
17
+ end
18
+
19
+ def settings_class
20
+ Settings.const_get(self.class.name.demodulize) rescue Settings::Query
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ module Compendium::Presenters::Settings
2
+ class Query
3
+ delegate :[], :fetch, to: :@settings
4
+
5
+ def initialize
6
+ @settings = {}.with_indifferent_access
7
+ end
8
+
9
+ def method_missing(name, *args, &block)
10
+ if block_given?
11
+ @settings[name] = block.call(*args)
12
+ elsif !args.empty?
13
+ @settings[name] = args.length == 1 ? args.first : args
14
+ elsif name.to_s.end_with?('?')
15
+ prefix = name.to_s.gsub(/\?\z/, '')
16
+ @settings.key?(prefix)
17
+ else
18
+ @settings[name]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ module Compendium::Presenters::Settings
2
+ class Table < Query
3
+ attr_reader :headings
4
+
5
+ def initialize(headings)
6
+ super()
7
+ @headings = Hash[headings.zip(headings)].with_indifferent_access
8
+ end
9
+
10
+ def override_heading(col, label)
11
+ @headings[col] = label
12
+ end
13
+
14
+ def format(column, &block)
15
+ @settings[:formatters] ||= {}
16
+ @settings[:formatters][column] = block
17
+ end
18
+
19
+ def formatters
20
+ (@settings[:formatters] || {})
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,81 @@
1
+ module Compendium::Presenters
2
+ class Table < Query
3
+ attr_reader :records, :totals
4
+
5
+ def initialize(*)
6
+ super
7
+
8
+ @records = results.records
9
+ @totals = @records.pop if has_totals_row?
10
+
11
+ @settings = settings_class.new(results.keys)
12
+ yield @settings if block_given?
13
+ end
14
+
15
+ def render
16
+ content_tag(:table, class: 'results') do
17
+ table = ActiveSupport::SafeBuffer.new
18
+ table << content_tag(:thead, build_heading_row)
19
+ table << content_tag(:tbody) do
20
+ tbody = ActiveSupport::SafeBuffer.new
21
+ records.each { |row| tbody << build_data_row(row) }
22
+ tbody
23
+ end
24
+ table << content_tag(:tfoot, build_totals_row) if has_totals_row?
25
+ table
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def headings
32
+ @settings.headings
33
+ end
34
+
35
+ def has_totals_row?
36
+ query.options.fetch(:totals, false)
37
+ end
38
+
39
+ def build_data_row(row)
40
+ build_row(row, 'data') { |key, val| formatted_value(key, val) }
41
+ end
42
+
43
+ def build_heading_row
44
+ build_row(headings, 'headings', :th) { |key, val| t(val) }
45
+ end
46
+
47
+ def build_totals_row
48
+ totals[totals.keys.first] = t(:total)
49
+ build_row(totals, 'totals', :th) { |key, val| formatted_value(key, val) }
50
+ end
51
+
52
+ def build_row(row, row_class, cell_type = :td)
53
+ content_tag('tr', class: row_class) do
54
+ out = ActiveSupport::SafeBuffer.new
55
+
56
+ row.each.with_index do |(key, val), i|
57
+ val = yield key, val, i if block_given?
58
+ out << content_tag(cell_type, val)
59
+ end
60
+
61
+ out
62
+ end
63
+ end
64
+
65
+ def formatted_value(k, v)
66
+ if @settings.formatters[k]
67
+ @settings.formatters[k].call(v)
68
+ else
69
+ if v.numeric?
70
+ if v.zero? and @settings.display_zero_as?
71
+ @settings.display_zero_as
72
+ else
73
+ sprintf(@settings.number_format || '%0.2f', v)
74
+ end
75
+ elsif v.nil?
76
+ @settings.display_nil_as
77
+ end
78
+ end || v
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,52 @@
1
+ module Compendium
2
+ class ReportsController < ::ApplicationController
3
+ helper Compendium::ReportsHelper
4
+
5
+ before_filter :find_report
6
+ before_filter :run_report, only: :run
7
+
8
+ def setup
9
+ render locals: { report: setup_report, prefix: @prefix }
10
+ end
11
+
12
+ def run
13
+ template = template_exists?(@prefix, get_template_prefixes) ? @prefix : 'run'
14
+ render action: template, locals: { report: @report }
15
+ end
16
+
17
+ private
18
+
19
+ def find_report
20
+ @prefix = params[:report_name]
21
+ @report_name = "#{@prefix}_report"
22
+
23
+ begin
24
+ require(@report_name) unless Rails.env.development? or Module.const_defined?(@report_name.classify)
25
+ @report_class = @report_name.camelize.constantize
26
+ rescue LoadError
27
+ flash[:error] = t(:invalid_report)
28
+ redirect_to action: :index
29
+ end
30
+ end
31
+
32
+ def setup_report
33
+ @report_class.new(params[:report] || {})
34
+ end
35
+
36
+ def run_report
37
+ @report = @report_class.new(params[:report]).run(self)
38
+ end
39
+
40
+ def get_template_prefixes
41
+ paths = []
42
+ klass = self.class
43
+
44
+ begin
45
+ paths << klass.name.underscore.gsub(/_controller$/, '')
46
+ klass = klass.superclass
47
+ end while(klass != ActionController::Base)
48
+
49
+ paths
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ module Compendium
2
+ module ReportsHelper
3
+ def expose(*args)
4
+ klass = args.pop if args.last.is_a?(Class)
5
+ klass ||= "Compendium::Presenters::#{args.first.class}".constantize
6
+ presenter = klass.new(self, *(args.empty? ? [nil] : args))
7
+ yield presenter if block_given?
8
+ presenter
9
+ end
10
+
11
+ def render_report_setup(assigns)
12
+ render file: "#{Compendium::Engine.root}/app/views/compendium/reports/setup", locals: assigns
13
+ end
14
+
15
+ def render_if_exists(options = {})
16
+ if lookup_context.template_exists?(options[:partial] || options[:template], options[:path], options.key?(:partial))
17
+ render options
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1 @@
1
+ = t(:results, report_name: @prefix)
@@ -0,0 +1,14 @@
1
+ - content_for :stylesheets do
2
+ = stylesheet_link_tag 'compendium/options'
3
+
4
+ = render_if_exists partial: 'report_header', path: 'compendium/reports'
5
+
6
+ .options
7
+ = form_for report, as: :report, url: compendium_reports_run_path do |f|
8
+ - report.options.values.each do |option|
9
+ - expose option, Compendium::Presenters::Option do |opt|
10
+ .option
11
+ = opt.label(f)
12
+ .option-element-group= opt.input(self, f)
13
+
14
+ = f.submit t(:generate_report, scope: 'compendium.reports')