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,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