quby-compiler 0.2.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 (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'