quby-compiler 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.gitlab-ci.yml +5 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Dockerfile +11 -0
  8. data/Gemfile +8 -0
  9. data/Gemfile.lock +133 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +44 -0
  12. data/Rakefile +6 -0
  13. data/bin/console +14 -0
  14. data/bin/rspec +29 -0
  15. data/bin/setup +8 -0
  16. data/config/locales/de.yml +58 -0
  17. data/config/locales/en.yml +57 -0
  18. data/config/locales/nl.yml +57 -0
  19. data/config/locales/rails-i18n/README.md +4 -0
  20. data/config/locales/rails-i18n/de.yml +223 -0
  21. data/config/locales/rails-i18n/en.yml +216 -0
  22. data/config/locales/rails-i18n/nl.yml +214 -0
  23. data/exe/quby-compile +56 -0
  24. data/lib/quby/array_attribute_valid_validator.rb +15 -0
  25. data/lib/quby/attribute_valid_validator.rb +14 -0
  26. data/lib/quby/compiler.rb +50 -0
  27. data/lib/quby/compiler/dsl.rb +29 -0
  28. data/lib/quby/compiler/dsl/base.rb +20 -0
  29. data/lib/quby/compiler/dsl/calls_custom_methods.rb +29 -0
  30. data/lib/quby/compiler/dsl/charting/bar_chart_builder.rb +14 -0
  31. data/lib/quby/compiler/dsl/charting/chart_builder.rb +95 -0
  32. data/lib/quby/compiler/dsl/charting/line_chart_builder.rb +34 -0
  33. data/lib/quby/compiler/dsl/charting/overview_chart_builder.rb +31 -0
  34. data/lib/quby/compiler/dsl/charting/radar_chart_builder.rb +14 -0
  35. data/lib/quby/compiler/dsl/helpers.rb +53 -0
  36. data/lib/quby/compiler/dsl/panel_builder.rb +80 -0
  37. data/lib/quby/compiler/dsl/question_builder.rb +40 -0
  38. data/lib/quby/compiler/dsl/questionnaire_builder.rb +279 -0
  39. data/lib/quby/compiler/dsl/questions/base.rb +180 -0
  40. data/lib/quby/compiler/dsl/questions/checkbox_question_builder.rb +20 -0
  41. data/lib/quby/compiler/dsl/questions/date_question_builder.rb +18 -0
  42. data/lib/quby/compiler/dsl/questions/deprecated_question_builder.rb +18 -0
  43. data/lib/quby/compiler/dsl/questions/float_question_builder.rb +21 -0
  44. data/lib/quby/compiler/dsl/questions/integer_question_builder.rb +21 -0
  45. data/lib/quby/compiler/dsl/questions/radio_question_builder.rb +20 -0
  46. data/lib/quby/compiler/dsl/questions/select_question_builder.rb +18 -0
  47. data/lib/quby/compiler/dsl/questions/string_question_builder.rb +20 -0
  48. data/lib/quby/compiler/dsl/questions/text_question_builder.rb +22 -0
  49. data/lib/quby/compiler/dsl/score_builder.rb +22 -0
  50. data/lib/quby/compiler/dsl/score_schema_builder.rb +53 -0
  51. data/lib/quby/compiler/dsl/standardized_panel_generators.rb +33 -0
  52. data/lib/quby/compiler/dsl/table_builder.rb +48 -0
  53. data/lib/quby/compiler/entities.rb +38 -0
  54. data/lib/quby/compiler/entities/charting/bar_chart.rb +17 -0
  55. data/lib/quby/compiler/entities/charting/chart.rb +101 -0
  56. data/lib/quby/compiler/entities/charting/charts.rb +42 -0
  57. data/lib/quby/compiler/entities/charting/line_chart.rb +38 -0
  58. data/lib/quby/compiler/entities/charting/overview_chart.rb +20 -0
  59. data/lib/quby/compiler/entities/charting/plottable.rb +20 -0
  60. data/lib/quby/compiler/entities/charting/radar_chart.rb +17 -0
  61. data/lib/quby/compiler/entities/definition.rb +26 -0
  62. data/lib/quby/compiler/entities/fields.rb +119 -0
  63. data/lib/quby/compiler/entities/flag.rb +55 -0
  64. data/lib/quby/compiler/entities/item.rb +40 -0
  65. data/lib/quby/compiler/entities/lookup_tables.rb +71 -0
  66. data/lib/quby/compiler/entities/outcome_table.rb +31 -0
  67. data/lib/quby/compiler/entities/panel.rb +82 -0
  68. data/lib/quby/compiler/entities/question.rb +365 -0
  69. data/lib/quby/compiler/entities/question_option.rb +96 -0
  70. data/lib/quby/compiler/entities/questionnaire.rb +440 -0
  71. data/lib/quby/compiler/entities/questions/checkbox_question.rb +82 -0
  72. data/lib/quby/compiler/entities/questions/date_question.rb +84 -0
  73. data/lib/quby/compiler/entities/questions/deprecated_question.rb +19 -0
  74. data/lib/quby/compiler/entities/questions/float_question.rb +15 -0
  75. data/lib/quby/compiler/entities/questions/integer_question.rb +15 -0
  76. data/lib/quby/compiler/entities/questions/radio_question.rb +19 -0
  77. data/lib/quby/compiler/entities/questions/select_question.rb +19 -0
  78. data/lib/quby/compiler/entities/questions/string_question.rb +15 -0
  79. data/lib/quby/compiler/entities/questions/text_question.rb +15 -0
  80. data/lib/quby/compiler/entities/score_calculation.rb +35 -0
  81. data/lib/quby/compiler/entities/score_schema.rb +25 -0
  82. data/lib/quby/compiler/entities/subscore_schema.rb +23 -0
  83. data/lib/quby/compiler/entities/table.rb +143 -0
  84. data/lib/quby/compiler/entities/text.rb +71 -0
  85. data/lib/quby/compiler/entities/textvar.rb +23 -0
  86. data/lib/quby/compiler/entities/validation.rb +17 -0
  87. data/lib/quby/compiler/entities/version.rb +23 -0
  88. data/lib/quby/compiler/entities/visibility_rule.rb +71 -0
  89. data/lib/quby/compiler/instance.rb +72 -0
  90. data/lib/quby/compiler/output.rb +13 -0
  91. data/lib/quby/compiler/outputs.rb +4 -0
  92. data/lib/quby/compiler/outputs/quby_frontend_v1_serializer.rb +362 -0
  93. data/lib/quby/compiler/outputs/quby_frontend_v2_serializer.rb +15 -0
  94. data/lib/quby/compiler/outputs/roqua_serializer.rb +108 -0
  95. data/lib/quby/compiler/outputs/seed_serializer.rb +34 -0
  96. data/lib/quby/compiler/services/definition_validator.rb +330 -0
  97. data/lib/quby/compiler/services/quby_proxy.rb +405 -0
  98. data/lib/quby/compiler/services/seed_diff.rb +116 -0
  99. data/lib/quby/compiler/services/text_transformation.rb +30 -0
  100. data/lib/quby/compiler/version.rb +5 -0
  101. data/lib/quby/markdown_parser.rb +38 -0
  102. data/lib/quby/range_categories.rb +38 -0
  103. data/lib/quby/settings.rb +86 -0
  104. data/lib/quby/text_transformation.rb +26 -0
  105. data/lib/quby/type_validator.rb +12 -0
  106. data/quby-compiler.gemspec +39 -0
  107. metadata +277 -0
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quby
4
+ module Compiler
5
+ module Entities
6
+ class Flag < Struct.new(:key, :description_true, :description_false, :description, :internal, :trigger_on,
7
+ :shows_questions, :hides_questions, :depends_on, :default_in_interface)
8
+ # rubocop:disable ParameterLists
9
+ def initialize(key:,
10
+ description_true: nil,
11
+ description_false: nil,
12
+ description: nil,
13
+ internal: false,
14
+ trigger_on: true,
15
+ shows_questions: [],
16
+ hides_questions: [],
17
+ depends_on: nil, # used in interface to hide this flag unless the depended on flag is set to true
18
+ default_in_interface: nil) # used in interface to set a default for the flag state,
19
+ # does not have an effect outside of the interface
20
+ super(key, description_true, description_false, description, internal, trigger_on, shows_questions,
21
+ hides_questions, depends_on, default_in_interface)
22
+ ensure_valid_descriptions
23
+ end
24
+ # rubocop:enable ParameterLists
25
+
26
+ def if_triggered_by(answer_flags)
27
+ yield if answer_flags[key] == trigger_on
28
+ end
29
+
30
+ def variable_description
31
+ "#{description} (true - '#{description_true}', false - '#{description_false}')"
32
+ end
33
+
34
+ def to_codebook(_options = {})
35
+ output = []
36
+ output << "#{key} flag"
37
+ output << "'#{description}'" if description.present?
38
+ output << " 'true' - #{description_true}"
39
+ output << " 'false' - #{description_false}"
40
+ output << " '' (leeg) - Vlag niet ingesteld, informatie onbekend"
41
+ output << ""
42
+ output.join("\n")
43
+ end
44
+
45
+ private
46
+
47
+ def ensure_valid_descriptions
48
+ unless (description_false.present? && description_true.present?) || description.present?
49
+ raise "Flag '#{key}' Requires at least either both description_true and description_false or a description"
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+
5
+ module Quby
6
+ module Compiler
7
+ module Entities
8
+ class Item
9
+ include ActiveModel::Validations
10
+ include ActiveSupport::Callbacks
11
+ define_callbacks :after_dsl_enhance
12
+
13
+ attr_accessor :presentation
14
+ attr_accessor :switch_cycle
15
+
16
+ # Raw content may contain a raw HTML replacement for this item
17
+ attr_accessor :raw_content
18
+
19
+ def initialize(options = {})
20
+ @raw_content = options[:raw_content]
21
+ @switch_cycle = options[:switch_cycle] || false
22
+ end
23
+
24
+ def presentation
25
+ @presentation || "vertical"
26
+ end
27
+
28
+ def as_json(options = {})
29
+ {
30
+ class: self.class.to_s
31
+ }
32
+ end
33
+
34
+ def to_codebook(questionnaire, options = {})
35
+ ""
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,71 @@
1
+ require 'csv'
2
+
3
+ module Quby
4
+ module Compiler
5
+ module Entities
6
+ class LookupTables
7
+ def initialize(path)
8
+ @path = path
9
+ end
10
+
11
+ def fetch(key)
12
+ csv_path = File.join(@path, "#{key}.csv")
13
+ data = CSV.read(csv_path, col_sep: ';', skip_blanks: true)
14
+ headers = data.shift
15
+ compare = data.shift
16
+ self.class.from_csv(levels: headers, compare: compare, data: data)
17
+ end
18
+
19
+ # load csv data into a tree.
20
+ # each row is a path through the tree.
21
+ # String and float types are used to make an exact match.
22
+ # A range is always a range between two floats where the range is between
23
+ # the low value (inclusive) and the high value (exclusive),
24
+ # written as 4:5 (low:high). These boundaries can be given as floats or
25
+ # integers, but internally they are always treated as a floats.
26
+ # The low and high values of a range cannot be equal.
27
+ # Use minfinity or infinity to create infinite ranges.
28
+ #
29
+ # @params levels [Array<String>] An array of column names
30
+ # @param compare [Array<String>]An array of lookup types (string, float or range) for each column
31
+ # @param data [Array<Array<>>] The rows describing a path through the tree.
32
+ def self.from_csv(levels:, compare:, data:)
33
+ tree = data.each_with_object({}) do |row, tree|
34
+ add_to_tree(tree, row, levels, compare)
35
+ end
36
+ {levels: levels, tree: tree}
37
+ end
38
+
39
+ private
40
+
41
+ def self.add_to_tree(tree, (value, *path), (level, *levels), (compare, *compares))
42
+ key = case compare
43
+ when 'string' then value.to_s
44
+ when 'float' then parse_float(value)
45
+ when 'range' then create_range(value)
46
+ end
47
+
48
+ if levels.empty?
49
+ return key
50
+ end
51
+
52
+ tree.merge! key => add_to_tree(tree[key] || {}, path, levels, compares)
53
+ end
54
+
55
+ def self.create_range(value)
56
+ min, max = value.split(':').map { |val| parse_float(val) }
57
+ fail 'Cannot create range between two equal values' if min == max
58
+ (min...max)
59
+ end
60
+
61
+ def self.parse_float(value)
62
+ case value
63
+ when 'infinity' then Float::INFINITY
64
+ when 'minfinity' then -Float::INFINITY
65
+ else Float(value)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,31 @@
1
+ module Quby
2
+ module Compiler
3
+ module Entities
4
+ # OutcomeTable describes how scores are formatted in a table in outcome views
5
+ # @param key [Symbol] key to reference this outcome table by
6
+ # @param score_keys [Array<Symbol>] which scores are selected for the rows of the table
7
+ # @param subscore_keys [Array<Symbol>] which subscores (:value, :interpretation etc.) make up the table columns
8
+ # @param name [String] a title that will be shown above the table
9
+ # @param default_collapsed [Boolean] if true, collapses the table to only show the name by default
10
+ # @param questionnaire [Questionnaire] for validating score keys and subscore keys according to its score_schema
11
+ class OutcomeTable
12
+ include ActiveModel::Model
13
+ attr_accessor :score_keys, :subscore_keys, :name, :default_collapsed, :questionnaire, :key
14
+
15
+ validates :score_keys, :subscore_keys, :questionnaire, :key, presence: true
16
+ validates :name, presence: true, if: proc { |table| table.default_collapsed }
17
+ validate :references_existing_score_keys
18
+
19
+ def references_existing_score_keys
20
+ (score_keys - questionnaire.score_schemas.values.map(&:key)).each do |missing_key|
21
+ errors.add :score_keys, "#{missing_key.inspect} not found in score schemas"
22
+ end
23
+ existing_subscore_keys = questionnaire.score_schemas.values.flat_map(&:subscore_schemas).map(&:key)
24
+ (subscore_keys - existing_subscore_keys).each do |missing_key|
25
+ errors.add :subscore_keys, "#{missing_key.inspect} not found in subscore schemas"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'quby/compiler/entities'
4
+
5
+ module Quby
6
+ module Compiler
7
+ module Entities
8
+ class Panel < Item
9
+ attr_accessor :title
10
+ attr_accessor :items
11
+ attr_accessor :key
12
+ attr_reader :questionnaire
13
+
14
+ def initialize(options = {})
15
+ @questionnaire = options[:questionnaire]
16
+ @title = options[:title]
17
+ @key = options[:key]
18
+ @items = options[:items] || []
19
+ end
20
+
21
+ def as_json(options = {})
22
+ super.merge(title: title, index: index, items: json_items)
23
+ end
24
+
25
+ def index
26
+ @questionnaire.panels.index(self)
27
+ end
28
+
29
+ def next
30
+ this_panel_index = index
31
+
32
+ if this_panel_index < @questionnaire.panels.size
33
+ return @questionnaire.panels[this_panel_index + 1]
34
+ else
35
+ nil
36
+ end
37
+ end
38
+
39
+ def prev
40
+ this_panel_index = index
41
+
42
+ if this_panel_index > 0
43
+ return @questionnaire.panels[this_panel_index - 1]
44
+ else
45
+ nil
46
+ end
47
+ end
48
+
49
+ def json_items
50
+ items.map do |item|
51
+ case item
52
+ when Text
53
+ { type: 'html', html: item.html }
54
+ when Question
55
+ next if item.table # things inside a table are added to the table, AND ALSO to the panel. skip them.
56
+ { type: 'question', key: item.key }
57
+ when Table
58
+ { type: "table" }
59
+ end
60
+ end.compact
61
+ end
62
+
63
+ def validations
64
+ vals = {}
65
+ items.each do |item|
66
+ if item.is_a? Question
67
+ item.options.each do |opt|
68
+ if opt.questions
69
+ opt.questions.each do |q|
70
+ vals[q.key] = q.validations
71
+ end
72
+ end
73
+ end
74
+ vals[item.key] = item.validations
75
+ end
76
+ end
77
+ vals
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,365 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'quby/compiler/entities/item'
4
+
5
+ module Quby
6
+ module Compiler
7
+ module Entities
8
+ class Question < Item
9
+ MARKDOWN_ATTRIBUTES = %w(description title).freeze
10
+
11
+ set_callback :after_dsl_enhance, :expand_depends_on_input_keys
12
+
13
+ # Standard attributes
14
+ attr_accessor :key
15
+ validates :key, presence: true, 'quby/type': {is_a: Symbol}
16
+ attr_accessor :sbg_key
17
+ attr_accessor :questionnaire
18
+ attr_accessor :title
19
+ attr_accessor :context_free_title
20
+ attr_accessor :description
21
+
22
+ attr_accessor :labels
23
+
24
+ # What kind of question is this?
25
+ attr_accessor :type
26
+
27
+ # How should we display this question
28
+ attr_accessor :as
29
+
30
+ # To hide old questions
31
+ attr_accessor :hidden
32
+
33
+ # Whether to skip the uniqueness validation on radio and select option values
34
+ attr_reader :allow_duplicate_option_values
35
+
36
+ # In what modes do we display this question
37
+ # NOTE We always display questions in print-view (if they have an answer)
38
+ attr_accessor :display_modes
39
+
40
+ # Multiple-choice questions have options to choose from
41
+ attr_accessor :options
42
+
43
+ # Question validation fails when there are no title and no context_free_title.
44
+ # When :allow_blank_titles => true passed, validation does not fail. Any other value will raise the failure.
45
+ attr_accessor :allow_blank_titles
46
+
47
+ # Minimum and maximum values for float and integer types
48
+ attr_accessor :minimum
49
+ attr_accessor :maximum
50
+
51
+ # Whether the browser should autocomplete this question (off by default)
52
+ attr_accessor :autocomplete
53
+
54
+ # Whether we show the value for each option
55
+ # :all => in all questionnaire display modes
56
+ # :none => in none of display modes
57
+ # :paged => for only in :paged display mode
58
+ # :bulk => for only in :bulk display mode
59
+ attr_accessor :show_values
60
+ validates :show_values, inclusion: {
61
+ in: [:all, :none, :paged, :bulk, :print],
62
+ message: "option invalid: %{value}. Valid options: :all, :none, :paged, :bulk)" }
63
+
64
+ # Structuring
65
+ attr_accessor :validations
66
+ attr_accessor :dependencies
67
+
68
+ # To display unit for number items
69
+ attr_accessor :unit
70
+ # To specify size of string/number input boxes
71
+ attr_accessor :size
72
+
73
+ # Whether this radio question is deselectable
74
+ attr_accessor :deselectable
75
+
76
+ # Some questions are a tree.
77
+ attr_accessor :parent
78
+ attr_accessor :parent_option_key
79
+
80
+ # Whether we can collapse this in bulk view
81
+ attr_accessor :disallow_bulk
82
+
83
+ # This question should not validate itself unless the depends_on question is answered.
84
+ # May also be an array of "#{question_key}_#{option_key}" strings that specify options
85
+ # this question depends on.
86
+ attr_accessor :depends_on
87
+
88
+ # Extra data hash to store on the question item's html element
89
+ attr_accessor :extra_data
90
+
91
+ # data-attributes for the input tag.
92
+ attr_accessor :input_data
93
+
94
+ # Whether we use the :description, the :value or :none for the score header above this question
95
+ attr_accessor :score_header
96
+
97
+ # options for grouping questions and setting a minimum or maximum number of answered questions in the group
98
+ attr_accessor :question_group
99
+ attr_accessor :group_minimum_answered
100
+ attr_accessor :group_maximum_answered
101
+
102
+ # Text variable name that will be replaced with the answer to this question
103
+ # In all following text elements that support markdown
104
+ attr_accessor :sets_textvar
105
+
106
+ # Amount of rows and cols a textarea has
107
+ attr_accessor :lines
108
+ attr_accessor :cols
109
+
110
+ # Table this question might belong to
111
+ attr_accessor :table
112
+
113
+ # In case of being displayed inside a table, amount of columns/rows to span
114
+ attr_accessor :col_span
115
+ attr_accessor :row_span
116
+
117
+ attr_accessor :default_invisible
118
+
119
+ # Slider only: where to place the sliding thing by default
120
+ # Can have value :hidden for a hidden handle.
121
+ attr_accessor :default_position
122
+
123
+ ##########################################################
124
+
125
+ # rubocop:disable CyclomaticComplexity, Metrics/MethodLength
126
+ def initialize(key, options = {})
127
+ super(options)
128
+
129
+ @extra_data ||= {}
130
+ @options = []
131
+ @allow_duplicate_option_values = options[:allow_duplicate_option_values]
132
+ @questionnaire = options[:questionnaire]
133
+ @key = key
134
+ @sbg_key = options[:sbg_key]
135
+ @type = options[:type]
136
+ @as = options[:as]
137
+ @title = options[:title]
138
+ @context_free_title = options[:context_free_title]
139
+ @allow_blank_titles = options[:allow_blank_titles]
140
+ @description = options[:description]
141
+ @display_modes = options[:display_modes]
142
+ @presentation = options[:presentation]
143
+ @validations = []
144
+ @parent = options[:parent]
145
+ @hidden = options[:hidden]
146
+ @table = options[:table]
147
+ @parent_option_key = options[:parent_option_key]
148
+ @autocomplete = options[:autocomplete] || "off"
149
+ @show_values = options[:show_values] || :bulk
150
+ @deselectable = (options[:deselectable].nil? || options[:deselectable])
151
+ @disallow_bulk = options[:disallow_bulk]
152
+ @score_header = options[:score_header] || :none
153
+ @sets_textvar = "#{questionnaire.key}_#{options[:sets_textvar]}" if options[:sets_textvar]
154
+ @unit = options[:unit]
155
+ @lines = options[:lines] || 6
156
+ @cols = options[:cols] || 40
157
+ @default_invisible = options[:default_invisible] || false
158
+ @labels ||= []
159
+
160
+ @col_span = options[:col_span] || 1
161
+ @row_span = options[:row_span] || 1
162
+
163
+ set_depends_on(options[:depends_on])
164
+
165
+ @question_group = options[:question_group]
166
+ @group_minimum_answered = options[:group_minimum_answered]
167
+ @group_maximum_answered = options[:group_maximum_answered]
168
+
169
+ @input_data = {}
170
+ @input_data[:value_tooltip] = true if options[:value_tooltip]
171
+
172
+ # Require subquestions of required questions by default
173
+ options[:required] = true if @parent&.validations&.first&.fetch(:type, nil) == :requires_answer
174
+ @validations << {type: :requires_answer, explanation: options[:error_explanation]} if options[:required]
175
+
176
+ if @type == :float
177
+ @validations << {type: :valid_float, explanation: options[:error_explanation]}
178
+ elsif @type == :integer
179
+ @validations << {type: :valid_integer, explanation: options[:error_explanation]}
180
+ end
181
+
182
+ if options[:minimum] and (@type == :integer || @type == :float)
183
+ fail "deprecated" # pretty sure this is not used anywhere
184
+ end
185
+ if options[:maximum] and (@type == :integer || @type == :float)
186
+ fail "deprecated" # pretty sure this is not used anywhere
187
+ end
188
+ @default_position = options[:default_position]
189
+
190
+ if @question_group
191
+ if @group_minimum_answered
192
+ @validations << {type: :answer_group_minimum, group: @question_group, value: @group_minimum_answered,
193
+ explanation: options[:error_explanation]}
194
+ end
195
+ if @group_maximum_answered
196
+ @validations << {type: :answer_group_maximum, group: @question_group, value: @group_maximum_answered,
197
+ explanation: options[:error_explanation]}
198
+ end
199
+ end
200
+ end
201
+ # rubocop:enable CyclomaticComplexity, Metrics/MethodLength
202
+
203
+ # rubocop:disable AccessorMethodName
204
+ def set_depends_on(keys)
205
+ return if keys.blank?
206
+ keys = [keys] unless keys.is_a?(Array)
207
+ @depends_on = keys
208
+ end
209
+ # rubocop:enable AccessorMethodName
210
+
211
+ def context_free_title
212
+ @context_free_title || @title
213
+ end
214
+
215
+ def expand_depends_on_input_keys
216
+ return unless @depends_on
217
+ @depends_on = questionnaire.expand_input_keys(@depends_on)
218
+ @extra_data[:"depends-on"] = @depends_on.to_json
219
+ rescue => e
220
+ raise e.class, "Question #{key} depends_on contains an error: #{e.message}"
221
+ end
222
+
223
+ def col_span
224
+ options.length > 0 && type != :select ? options.length : @col_span
225
+ end
226
+
227
+ def as_json(options = {})
228
+ # rubocop:disable SymbolName
229
+ super.merge(
230
+ key: key,
231
+ title: Quby::MarkdownParser.new(title).to_html,
232
+ description: Quby::MarkdownParser.new(description).to_html,
233
+ type: type,
234
+ unit: unit,
235
+ size: size,
236
+ hidden: hidden?,
237
+ displayModes: display_modes,
238
+ defaultInvisible: default_invisible,
239
+ viewSelector: view_selector,
240
+ parentKey: parent&.key,
241
+ parentOptionKey: parent_option_key,
242
+ deselectable: deselectable,
243
+ presentation: presentation,
244
+ as: as,
245
+ questionGroup: question_group
246
+ )
247
+ end
248
+
249
+ # Returns all keys belonging to html inputs generated by this question.
250
+ def input_keys
251
+ if options.blank?
252
+ answer_keys
253
+ else
254
+ # Some options don't have a key (inner_title), they are stripped
255
+ options.map { |opt| opt.input_key }.compact
256
+ end
257
+ end
258
+
259
+ def key_in_use?(k)
260
+ claimed_keys.include?(k) ||
261
+ options.any? { |option| option.key_in_use?(k) }
262
+ end
263
+
264
+ # The keys this question claims as his own. Not including options and subquestions.
265
+ # Includes keys for the question, inputs and answers.
266
+ def claimed_keys
267
+ [key]
268
+ end
269
+
270
+ # Returns all possible answer keys of this question (excluding subquestions, including options).
271
+ # radio/select/scale-options do not create answer_keys, but answer values.
272
+ def answer_keys
273
+ [key]
274
+ end
275
+
276
+ def default_position
277
+ return unless as == :slider
278
+ half = (type == :float) ? 2.0 : 2
279
+ @default_position || ((minimum + maximum) / half if minimum && maximum)
280
+ end
281
+
282
+ def minimum
283
+ validations.find { |i| i[:type] == :minimum }.try(:fetch, :value)
284
+ end
285
+
286
+ def maximum
287
+ validations.find { |i| i[:type] == :maximum }.try(:fetch, :value)
288
+ end
289
+
290
+ def html_id
291
+ "answer_#{key}_input"
292
+ end
293
+
294
+ def view_selector
295
+ table.blank? ? "#item_#{key}" : "[data-for=#{key}], #answer_#{key}_input"
296
+ end
297
+
298
+ def hidden?
299
+ hidden
300
+ end
301
+
302
+ def show_values_in_mode?(mode)
303
+ case show_values
304
+ when :none then false
305
+ when :all then true
306
+ else show_values == mode
307
+ end
308
+ end
309
+
310
+ def subquestions
311
+ options.map { |opt| opt.questions }.flatten
312
+ end
313
+
314
+ def subquestion?
315
+ !parent_option_key.nil?
316
+ end
317
+
318
+ def to_codebook(questionnaire, opts = {})
319
+ output = []
320
+ question_key = codebook_key(key, questionnaire, opts)
321
+ output << "#{question_key} #{codebook_output_type} #{codebook_output_range}#{' deprecated' if hidden}"
322
+ output << "\"#{context_free_title}\"" unless context_free_title.blank?
323
+ options_string = options.map do |option|
324
+ option.to_codebook(questionnaire, opts)
325
+ end.compact.join("\n")
326
+ output << options_string unless options.blank?
327
+ output.join("\n")
328
+ end
329
+
330
+ def codebook_key(key, questionnaire, opts = {})
331
+ key.to_s.gsub(/^v_/, "#{opts[:roqua_key] || questionnaire.key.to_s}_")
332
+ end
333
+
334
+ def codebook_output_type
335
+ type
336
+ end
337
+
338
+ def codebook_output_range
339
+ range_min = validations.find { |i| i[:type] == :minimum }&.fetch(:value, nil)
340
+ range_max = validations.find { |i| i[:type] == :maximum }&.fetch(:value, nil)
341
+
342
+ if range_min || range_max
343
+ "(#{[range_min, "value", range_max].compact.join(" &lt;= ")})"
344
+ else
345
+ ""
346
+ end
347
+ end
348
+
349
+ def variable_descriptions
350
+ {key => context_free_title}.with_indifferent_access
351
+ end
352
+ end
353
+ end
354
+ end
355
+ end
356
+
357
+ require 'quby/compiler/entities/questions/checkbox_question'
358
+ require 'quby/compiler/entities/questions/date_question'
359
+ require 'quby/compiler/entities/questions/deprecated_question'
360
+ require 'quby/compiler/entities/questions/float_question'
361
+ require 'quby/compiler/entities/questions/integer_question'
362
+ require 'quby/compiler/entities/questions/radio_question'
363
+ require 'quby/compiler/entities/questions/select_question'
364
+ require 'quby/compiler/entities/questions/string_question'
365
+ require 'quby/compiler/entities/questions/text_question'