quby-compiler 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.gitlab-ci.yml +5 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dockerfile +11 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +133 -0
- data/LICENSE.txt +21 -0
- data/README.md +44 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/rspec +29 -0
- data/bin/setup +8 -0
- data/config/locales/de.yml +58 -0
- data/config/locales/en.yml +57 -0
- data/config/locales/nl.yml +57 -0
- data/config/locales/rails-i18n/README.md +4 -0
- data/config/locales/rails-i18n/de.yml +223 -0
- data/config/locales/rails-i18n/en.yml +216 -0
- data/config/locales/rails-i18n/nl.yml +214 -0
- data/exe/quby-compile +56 -0
- data/lib/quby/array_attribute_valid_validator.rb +15 -0
- data/lib/quby/attribute_valid_validator.rb +14 -0
- data/lib/quby/compiler.rb +50 -0
- data/lib/quby/compiler/dsl.rb +29 -0
- data/lib/quby/compiler/dsl/base.rb +20 -0
- data/lib/quby/compiler/dsl/calls_custom_methods.rb +29 -0
- data/lib/quby/compiler/dsl/charting/bar_chart_builder.rb +14 -0
- data/lib/quby/compiler/dsl/charting/chart_builder.rb +95 -0
- data/lib/quby/compiler/dsl/charting/line_chart_builder.rb +34 -0
- data/lib/quby/compiler/dsl/charting/overview_chart_builder.rb +31 -0
- data/lib/quby/compiler/dsl/charting/radar_chart_builder.rb +14 -0
- data/lib/quby/compiler/dsl/helpers.rb +53 -0
- data/lib/quby/compiler/dsl/panel_builder.rb +80 -0
- data/lib/quby/compiler/dsl/question_builder.rb +40 -0
- data/lib/quby/compiler/dsl/questionnaire_builder.rb +279 -0
- data/lib/quby/compiler/dsl/questions/base.rb +180 -0
- data/lib/quby/compiler/dsl/questions/checkbox_question_builder.rb +20 -0
- data/lib/quby/compiler/dsl/questions/date_question_builder.rb +18 -0
- data/lib/quby/compiler/dsl/questions/deprecated_question_builder.rb +18 -0
- data/lib/quby/compiler/dsl/questions/float_question_builder.rb +21 -0
- data/lib/quby/compiler/dsl/questions/integer_question_builder.rb +21 -0
- data/lib/quby/compiler/dsl/questions/radio_question_builder.rb +20 -0
- data/lib/quby/compiler/dsl/questions/select_question_builder.rb +18 -0
- data/lib/quby/compiler/dsl/questions/string_question_builder.rb +20 -0
- data/lib/quby/compiler/dsl/questions/text_question_builder.rb +22 -0
- data/lib/quby/compiler/dsl/score_builder.rb +22 -0
- data/lib/quby/compiler/dsl/score_schema_builder.rb +53 -0
- data/lib/quby/compiler/dsl/standardized_panel_generators.rb +33 -0
- data/lib/quby/compiler/dsl/table_builder.rb +48 -0
- data/lib/quby/compiler/entities.rb +38 -0
- data/lib/quby/compiler/entities/charting/bar_chart.rb +17 -0
- data/lib/quby/compiler/entities/charting/chart.rb +101 -0
- data/lib/quby/compiler/entities/charting/charts.rb +42 -0
- data/lib/quby/compiler/entities/charting/line_chart.rb +38 -0
- data/lib/quby/compiler/entities/charting/overview_chart.rb +20 -0
- data/lib/quby/compiler/entities/charting/plottable.rb +20 -0
- data/lib/quby/compiler/entities/charting/radar_chart.rb +17 -0
- data/lib/quby/compiler/entities/definition.rb +26 -0
- data/lib/quby/compiler/entities/fields.rb +119 -0
- data/lib/quby/compiler/entities/flag.rb +55 -0
- data/lib/quby/compiler/entities/item.rb +40 -0
- data/lib/quby/compiler/entities/lookup_tables.rb +71 -0
- data/lib/quby/compiler/entities/outcome_table.rb +31 -0
- data/lib/quby/compiler/entities/panel.rb +82 -0
- data/lib/quby/compiler/entities/question.rb +365 -0
- data/lib/quby/compiler/entities/question_option.rb +96 -0
- data/lib/quby/compiler/entities/questionnaire.rb +440 -0
- data/lib/quby/compiler/entities/questions/checkbox_question.rb +82 -0
- data/lib/quby/compiler/entities/questions/date_question.rb +84 -0
- data/lib/quby/compiler/entities/questions/deprecated_question.rb +19 -0
- data/lib/quby/compiler/entities/questions/float_question.rb +15 -0
- data/lib/quby/compiler/entities/questions/integer_question.rb +15 -0
- data/lib/quby/compiler/entities/questions/radio_question.rb +19 -0
- data/lib/quby/compiler/entities/questions/select_question.rb +19 -0
- data/lib/quby/compiler/entities/questions/string_question.rb +15 -0
- data/lib/quby/compiler/entities/questions/text_question.rb +15 -0
- data/lib/quby/compiler/entities/score_calculation.rb +35 -0
- data/lib/quby/compiler/entities/score_schema.rb +25 -0
- data/lib/quby/compiler/entities/subscore_schema.rb +23 -0
- data/lib/quby/compiler/entities/table.rb +143 -0
- data/lib/quby/compiler/entities/text.rb +71 -0
- data/lib/quby/compiler/entities/textvar.rb +23 -0
- data/lib/quby/compiler/entities/validation.rb +17 -0
- data/lib/quby/compiler/entities/version.rb +23 -0
- data/lib/quby/compiler/entities/visibility_rule.rb +71 -0
- data/lib/quby/compiler/instance.rb +72 -0
- data/lib/quby/compiler/output.rb +13 -0
- data/lib/quby/compiler/outputs.rb +4 -0
- data/lib/quby/compiler/outputs/quby_frontend_v1_serializer.rb +362 -0
- data/lib/quby/compiler/outputs/quby_frontend_v2_serializer.rb +15 -0
- data/lib/quby/compiler/outputs/roqua_serializer.rb +108 -0
- data/lib/quby/compiler/outputs/seed_serializer.rb +34 -0
- data/lib/quby/compiler/services/definition_validator.rb +330 -0
- data/lib/quby/compiler/services/quby_proxy.rb +405 -0
- data/lib/quby/compiler/services/seed_diff.rb +116 -0
- data/lib/quby/compiler/services/text_transformation.rb +30 -0
- data/lib/quby/compiler/version.rb +5 -0
- data/lib/quby/markdown_parser.rb +38 -0
- data/lib/quby/range_categories.rb +38 -0
- data/lib/quby/settings.rb +86 -0
- data/lib/quby/text_transformation.rb +26 -0
- data/lib/quby/type_validator.rb +12 -0
- data/quby-compiler.gemspec +39 -0
- 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])/, '<\1')).gsub("<", "<")
|
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
|