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,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quby
4
+ module Compiler
5
+ module Entities
6
+ class QuestionOption
7
+ MARKDOWN_ATTRIBUTES = %w(description).freeze
8
+
9
+ attr_reader :key
10
+ attr_reader :value
11
+ attr_reader :description
12
+ attr_reader :questions
13
+ attr_reader :inner_title
14
+ attr_reader :hides_questions
15
+ attr_reader :shows_questions
16
+ attr_reader :hidden
17
+ attr_reader :placeholder
18
+ attr_reader :question
19
+ attr_reader :view_id
20
+ attr_reader :input_key
21
+
22
+ attr_reader :start_chosen
23
+
24
+ def initialize(key, question, options = {})
25
+ @key = key
26
+ @question = question
27
+ @value = options[:value]
28
+ @description = options[:description]
29
+ @context_free_description = options[:context_free_description]
30
+ @questions = []
31
+ @inner_title = options[:inner_title]
32
+ @hides_questions = options[:hides_questions] || []
33
+ @shows_questions = options[:shows_questions] || []
34
+ @hidden = options[:hidden] || false
35
+ @placeholder = options[:placeholder] || false
36
+ question.extra_data[:placeholder] = key if @placeholder
37
+
38
+ @input_key = (question.type == :check_box ? @key : "#{question.key}_#{key}".to_sym)
39
+ @view_id = "answer_#{input_key}"
40
+ end
41
+
42
+ def inner_title?
43
+ inner_title.present?
44
+ end
45
+
46
+ def key_in_use?(k)
47
+ return true if k == input_key
48
+ @questions.each { |q| return true if q.key_in_use?(k) }
49
+ false
50
+ end
51
+
52
+ def context_free_description
53
+ @context_free_description || @description
54
+ end
55
+
56
+ def as_json(options = {})
57
+ {
58
+ key: key,
59
+ value: value,
60
+ description: Quby::MarkdownParser.new(description).to_html,
61
+ context_free_description: Quby::MarkdownParser.new(context_free_description).to_html,
62
+ questions: questions,
63
+ innerTitle: inner_title,
64
+ hidesQuestions: hides_questions,
65
+ showsQuestions: shows_questions,
66
+ hidden: hidden,
67
+ placeholder: placeholder,
68
+ viewId: view_id
69
+ }
70
+ end
71
+
72
+ def to_codebook(questionnaire, opts)
73
+ return nil if inner_title
74
+ output = []
75
+
76
+ if question.type == :check_box
77
+ option_key = question.codebook_key(key, questionnaire, opts)
78
+ output << "#{option_key} #{question.codebook_output_type}#{' deprecated' if hidden || question.hidden }"
79
+ output << "\"#{question.title} -- #{description}\"" unless question.title.blank? and description.blank?
80
+ output << "1\tChecked"
81
+ output << "0\tUnchecked"
82
+ output << "empty\tUnchecked"
83
+ else
84
+ output << "#{value || key}\t\"#{description}\"#{' deprecated' if hidden}"
85
+ end
86
+
87
+ questions.each do |subquestion|
88
+ output << "\t#{subquestion.to_codebook(questionnaire, opts).gsub("\n", "\n\t")}"
89
+ end
90
+
91
+ output.join("\n")
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,440 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ require 'quby/settings'
5
+ require 'quby/compiler/entities/flag'
6
+ require 'quby/compiler/entities/textvar'
7
+ require 'quby/compiler/entities/validation'
8
+ require 'quby/compiler/entities/visibility_rule'
9
+
10
+ require 'action_view'
11
+ include ActionView::Helpers::SanitizeHelper
12
+
13
+ module Quby
14
+ module Compiler
15
+ module Entities
16
+ class Questionnaire
17
+ extend ActiveModel::Naming
18
+ include ActiveModel::Validations
19
+
20
+ class ValidationError < StandardError; end
21
+ class UnknownInputKey < ValidationError; end
22
+ class InputKeyAlreadyDefined < ValidationError; end
23
+
24
+ VALID_LICENSES = [:unknown,
25
+ :free, # freely available without license costs,
26
+ :pay_per_completion, # costs associated with each completed questionnaire,
27
+ :private, # not a publicly available questionnaire
28
+ :deprecated] # should no longer be used, hide from view
29
+
30
+ RESPONDENT_TYPES = %i( profess patient parent second_parent teacher caregiver )
31
+
32
+ def initialize(key, last_update: Time.now)
33
+ @key = key
34
+ @sbg_domains = []
35
+ @last_update = Time.at(last_update.to_i)
36
+ @score_calculations = {}.with_indifferent_access
37
+ @charts = Charting::Charts.new
38
+ @fields = Fields.new(self)
39
+ @license = :unknown
40
+ @renderer_version = :v1
41
+ @extra_css = ""
42
+ @allow_switch_to_bulk = false
43
+ @panels = []
44
+ @flags = {}.with_indifferent_access
45
+ @textvars = {}.with_indifferent_access
46
+ @language = :nl
47
+ @respondent_types = []
48
+ @tags = OpenStruct.new
49
+ @check_key_clashes = true
50
+ @validate_html = true
51
+ @score_schemas = {}.with_indifferent_access
52
+ @outcome_tables = []
53
+ @check_score_keys_consistency = true
54
+ @lookup_tables = {}
55
+ @versions = []
56
+ @seeds_patch = {}
57
+ end
58
+
59
+ attr_accessor :key
60
+ attr_accessor :title
61
+ attr_accessor :description
62
+ attr_accessor :outcome_description
63
+ attr_accessor :short_description
64
+ attr_accessor :roqua_keys
65
+ attr_accessor :sbg_key # not required to be unique
66
+ attr_accessor :sbg_domains
67
+ attr_accessor :versions
68
+ attr_accessor :abortable
69
+ attr_accessor :enable_previous_questionnaire_button
70
+ attr_accessor :panels
71
+ attr_accessor :score_calculations
72
+ attr_accessor :default_answer_value
73
+ attr_accessor :renderer_version
74
+ attr_accessor :leave_page_alert
75
+ attr_reader :fields
76
+ attr_accessor :extra_css
77
+ attr_accessor :allow_switch_to_bulk
78
+ attr_accessor :license
79
+ attr_accessor :licensor
80
+ attr_accessor :language
81
+ attr_accessor :respondent_types
82
+ attr_reader :tags # tags= is manually defined below
83
+ attr_accessor :outcome_regeneration_requested_at
84
+ attr_accessor :deactivate_answers_requested_at
85
+ attr_accessor :seeds_patch # a patch for the seeds, to define and fix changes to from-scratch generation
86
+ # whether to check for clashes between question input keys (HTML form keys)
87
+ attr_accessor :check_key_clashes
88
+ # whether to check consistency of score subkeys during seed generation
89
+ attr_accessor :check_score_keys_consistency
90
+ # If false, we don't check html for validity (for mate1 and mate1_pre)
91
+ attr_accessor :validate_html
92
+
93
+ attr_accessor :last_author
94
+ attr_accessor :allow_hotkeys # allow hotkeys for :all views, just :bulk views (default), or :none for never
95
+ attr_accessor :last_update
96
+
97
+ attr_accessor :charts
98
+
99
+ attr_accessor :flags
100
+ attr_accessor :textvars
101
+
102
+ attr_accessor :outcome_tables
103
+ attr_accessor :score_schemas
104
+ attr_accessor :lookup_tables
105
+
106
+ delegate :question_hash, :input_keys, :answer_keys, :expand_input_keys, to: :fields
107
+
108
+ def tags=(tags)
109
+ tags.each do |tag|
110
+ @tags[tag] = true
111
+ end
112
+ end
113
+
114
+ def leave_page_alert
115
+ return nil unless Settings.enable_leave_page_alert
116
+ @leave_page_alert || "Als u de pagina verlaat worden uw antwoorden niet opgeslagen."
117
+ end
118
+
119
+ def allow_hotkeys
120
+ (@allow_hotkeys || :bulk).to_s
121
+ end
122
+
123
+ def roqua_keys
124
+ @roqua_keys || [key]
125
+ end
126
+
127
+ def to_param
128
+ key
129
+ end
130
+
131
+ def add_panel(panel)
132
+ @panels << panel
133
+ end
134
+
135
+ def register_question(question)
136
+ fields.add(question)
137
+
138
+ if question.sets_textvar && !textvars.key?(question.sets_textvar)
139
+ fail "Undefined textvar: #{question.sets_textvar}"
140
+ end
141
+ end
142
+
143
+ def callback_after_dsl_enhance_on_questions
144
+ question_hash.each_value do |q|
145
+ q.run_callbacks :after_dsl_enhance
146
+ end
147
+ ensure_scores_have_schemas if Quby::Settings.require_score_schemas
148
+ end
149
+
150
+ def ensure_scores_have_schemas
151
+ missing_schemas = scores.map(&:key).map(&:to_s) - score_schemas.keys
152
+ missing_schemas.each do |key|
153
+ errors.add "Score #{key}", 'is missing a score schema'
154
+ end
155
+ end
156
+
157
+ def validate_questions
158
+ question_hash.each_value do |q|
159
+ unless q.valid?
160
+ q.errors.each { |attr, err| errors.add(attr, err) }
161
+ end
162
+ end
163
+ end
164
+
165
+ def questions_tree
166
+ return @questions_tree_cache if @questions_tree_cache
167
+
168
+ recurse = lambda do |question|
169
+ [question, question.subquestions.map(&recurse)]
170
+ end
171
+
172
+ @questions_tree_cache = (@panels && @panels.map do |panel|
173
+ panel.items.map { |item| recurse.call(item) if item.is_a?(Quby::Compiler::Entities::Question) }
174
+ end)
175
+ end
176
+
177
+ def questions
178
+ question_hash.values
179
+ end
180
+
181
+ def questions_of_type(type)
182
+ questions.select { |question| question.type == type }
183
+ end
184
+
185
+ def license=(type)
186
+ fail ArgumentError, 'Invalid license' unless VALID_LICENSES.include?(type)
187
+ @license = type
188
+ end
189
+
190
+ def as_json(options = {})
191
+ {
192
+ key: key,
193
+ title: title,
194
+ description: description,
195
+ outcomeDescription: outcome_description,
196
+ shortDescription: short_description,
197
+ panels: panels,
198
+ fields: fields,
199
+ flags: flags,
200
+ textvars: textvars,
201
+ validations: validations,
202
+ visibilityRules: visibility_rules
203
+ }
204
+ end
205
+
206
+ # rubocop:disable Metrics/MethodLength
207
+ def to_codebook(options = {})
208
+ output = []
209
+ output << title
210
+ output << "Date unknown"
211
+ output << ""
212
+
213
+ options[:extra_vars]&.each do |var|
214
+ output << "#{var[:key]} #{var[:type]}"
215
+ output << "\"#{var[:description]}\""
216
+ output << ""
217
+ end
218
+
219
+ top_questions = panels.map do |panel|
220
+ panel.items.select { |item| item.is_a? Question }
221
+ end.flatten.compact
222
+
223
+ top_questions.each do |question|
224
+ output << question.to_codebook(self, options)
225
+ output << ""
226
+ end
227
+
228
+ flags.each_value do |flag|
229
+ output << flag.to_codebook(options)
230
+ output << ""
231
+ end
232
+
233
+ textvars.each_value do |textvar|
234
+ output << textvar.to_codebook(options)
235
+ output << ""
236
+ end
237
+
238
+ output = output.join("\n")
239
+ strip_tags(output.gsub(/\<([ 1-9])/, '&lt;\1')).gsub("&lt;", "<")
240
+ end
241
+ # rubocop:enable Metrics/MethodLength
242
+
243
+ def key_in_use?(key)
244
+ fields.key_in_use?(key) || score_calculations.key?(key)
245
+ end
246
+
247
+ def add_score_calculation(builder)
248
+ if score_calculations.key?(builder.key)
249
+ fail InputKeyAlreadyDefined, "Score key `#{builder.key}` already defined."
250
+ end
251
+ score_calculations[builder.key] = builder
252
+ end
253
+
254
+ def add_score_schema(score_schema)
255
+ score_schemas[score_schema.key] = score_schema
256
+ end
257
+
258
+ def scores
259
+ score_calculations.values.select(&:score)
260
+ end
261
+
262
+ def find_plottable(key)
263
+ score_calculations[key] || question_hash.with_indifferent_access[key]
264
+ end
265
+
266
+ def actions
267
+ score_calculations.values.select(&:action)
268
+ end
269
+
270
+ def completion
271
+ score_calculations.values.select(&:completion).first
272
+ end
273
+
274
+ def add_chart(chart)
275
+ charts.add chart
276
+ end
277
+
278
+ def add_flag(flag_options)
279
+ if flag_options[:internal]
280
+ flag_key = flag_options[:key].to_sym
281
+ else
282
+ flag_key = "#{key}_#{flag_options[:key]}".to_sym
283
+ end
284
+ flag_options[:key] = flag_key
285
+ fail(ArgumentError, "Flag '#{flag_key}' already defined") if flags.key?(flag_key)
286
+ flags[flag_key] = Flag.new(flag_options)
287
+ end
288
+
289
+ def filter_flags(given_flags)
290
+ given_flags.select do |flag_key, _|
291
+ flags.key? flag_key
292
+ end
293
+ end
294
+
295
+ def add_textvar(textvar_options)
296
+ textvar_key = "#{key}_#{textvar_options.fetch(:key)}".to_sym
297
+ textvar_options[:key] = textvar_key
298
+ validate_textvar_keys_unique(textvar_key)
299
+ validate_depends_on_flag(textvar_key, textvar_options)
300
+ textvars[textvar_key] = Textvar.new(textvar_options)
301
+ end
302
+
303
+ def filter_textvars(given_textvars)
304
+ given_textvars.select do |textvar_key, _|
305
+ textvars.key? textvar_key
306
+ end
307
+ end
308
+
309
+ def default_textvars
310
+ textvars.select { |key, textvar| textvar.default.present? }
311
+ .map { |key, textvar| [key, textvar.default] }
312
+ .to_h
313
+ end
314
+
315
+ def answer_dsl_module # rubocop:disable Metrics/MethodLength
316
+ # Have to put this in a local variable so the module definition block can access it
317
+ questions_in_var = questions
318
+
319
+ @answer_dsl_cache ||= Module.new do
320
+ questions_in_var.each do |question|
321
+ next if question&.key.blank?
322
+ case question.type
323
+ when :date
324
+ question.components.each do |component|
325
+ # assignment to 'value' hash must be done under string keys
326
+ key = question.send("#{component}_key").to_s
327
+ define_method(key) do
328
+ self.value ||= Hash.new
329
+ self.value[key]
330
+ end
331
+
332
+ define_method("#{key}=") do |v|
333
+ self.value ||= Hash.new
334
+ self.value[key] = v&.strip
335
+ end
336
+ end
337
+
338
+ define_method(question.key) do
339
+ self.value ||= Hash.new
340
+
341
+ components = question.components.sort
342
+ component_values = components.map do |component|
343
+ value_key = question.send("#{component}_key").to_s
344
+ self.value[value_key]
345
+ end
346
+ case components
347
+ when [:day, :month, :year]
348
+ component_values.reverse.take_while { |p| p.present? }.reverse.join('-')
349
+ when [:month, :year]
350
+ component_values.reject(&:blank?).join('-')
351
+ when [:hour, :minute]
352
+ component_values.all?(&:blank?) ? '' : component_values.join(':')
353
+ end
354
+ end
355
+
356
+ when :check_box
357
+
358
+ define_method(question.key) do
359
+ self.value ||= Hash.new
360
+ self.value[question.key.to_s] ||= Hash.new
361
+ end
362
+
363
+ question.options.each do |opt|
364
+ next if opt&.key.blank?
365
+ define_method("#{opt.key}") do
366
+ self.value ||= Hash.new
367
+ self.value[question.key.to_s] ||= Hash.new
368
+ self.value[opt.key.to_s] ||= 0
369
+ end
370
+
371
+ define_method("#{opt.key}=") do |v|
372
+ v = v.to_i
373
+ self.value ||= Hash.new
374
+ self.value[question.key.to_s] ||= Hash.new
375
+ self.value[question.key.to_s][opt.key.to_s] = v
376
+ self.value[opt.key.to_s] = v
377
+ end
378
+ end
379
+ else
380
+ # Includes:
381
+ # question.type == :radio
382
+ # question.type == :scale
383
+ # question.type == :select
384
+ # question.type == :string
385
+ # question.type == :textarea
386
+ # question.type == :integer
387
+ # question.type == :float
388
+
389
+ define_method(question.key) do
390
+ self.value ||= Hash.new
391
+ self.value[question.key.to_s]
392
+ end
393
+
394
+ define_method(question.key.to_s + "=") do |v|
395
+ self.value ||= Hash.new
396
+ self.value[question.key.to_s] = v
397
+ end
398
+ end rescue nil
399
+ end
400
+ end
401
+ end
402
+
403
+ def add_outcome_table(outcome_table_options)
404
+ outcome_tables << OutcomeTable.new(**outcome_table_options, questionnaire: self)
405
+ end
406
+
407
+ def validations
408
+ @validations ||= fields.question_hash.values.flat_map do |question|
409
+ question.validations.map do |validation|
410
+ case validation[:type]
411
+ when :answer_group_minimum, :answer_group_maximum
412
+ Validation.new(validation.merge(field_keys: questions.select {|q| q.question_group == validation[:group]}.map(&:key)))
413
+ else
414
+ Validation.new(validation.merge(field_key: question.key))
415
+ end
416
+ end
417
+ end.uniq(&:config)
418
+ end
419
+
420
+ def visibility_rules
421
+ @visibility_rules ||= fields.question_hash.values.flat_map { |question| VisibilityRule.from(question) } \
422
+ + flags.values.flat_map { |flag| VisibilityRule.from_flag(flag) }
423
+ end
424
+
425
+ private
426
+
427
+ def validate_depends_on_flag(textvar_key, textvar_options)
428
+ if textvar_options[:depends_on_flag].present? && !flags.key?(textvar_options[:depends_on_flag])
429
+ fail(ArgumentError,
430
+ "Textvar '#{textvar_key}' depends on nonexistent flag '#{textvar_options[:depends_on_flag]}'")
431
+ end
432
+ end
433
+
434
+ def validate_textvar_keys_unique(textvar_key)
435
+ fail(ArgumentError, "Textvar '#{textvar_key}' already defined") if textvars.key?(textvar_key)
436
+ end
437
+ end
438
+ end
439
+ end
440
+ end