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