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,405 @@
|
|
|
1
|
+
# rubocop:disable all
|
|
2
|
+
require "active_support/all"
|
|
3
|
+
require 'roqua/core_ext/enumerable/sort_by_alphanum'
|
|
4
|
+
require 'quby/compiler/services/seed_diff'
|
|
5
|
+
|
|
6
|
+
module Quby
|
|
7
|
+
module Compiler
|
|
8
|
+
module Services
|
|
9
|
+
class QubyProxy
|
|
10
|
+
HEADERS = { value: "Score",
|
|
11
|
+
interpretation: "Interpretatie",
|
|
12
|
+
clin_interp: "Klinisch",
|
|
13
|
+
norm: "Norm",
|
|
14
|
+
tscore: "T-Score",
|
|
15
|
+
dimensie: "Dimensie",
|
|
16
|
+
mean: "Gemiddelde" }
|
|
17
|
+
|
|
18
|
+
attr_reader :questionnaire, :options
|
|
19
|
+
|
|
20
|
+
def initialize(questionnaire, options)
|
|
21
|
+
@questionnaire = questionnaire
|
|
22
|
+
@options = options
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def generate(seed)
|
|
26
|
+
question_titles = generate_question_titles
|
|
27
|
+
d_qtypes = {}
|
|
28
|
+
vars = []
|
|
29
|
+
@hidden_questions = {} # hash containing questions hidden by other questions
|
|
30
|
+
|
|
31
|
+
for question in questions_flat
|
|
32
|
+
if question.hidden && question.type != :check_box
|
|
33
|
+
d_qtypes[question.key.to_s] = { depends: :present } unless options[:without_depends]
|
|
34
|
+
end
|
|
35
|
+
unless question.hidden && (question.type == :check_box || question.type == :hidden)
|
|
36
|
+
vars << question.key.to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
case question.type
|
|
40
|
+
when :radio, :scale
|
|
41
|
+
handle_scale(question, question_titles, d_qtypes, vars)
|
|
42
|
+
when :select
|
|
43
|
+
d_qtypes[question.key.to_s] = { type: :discrete }
|
|
44
|
+
for option in question.options
|
|
45
|
+
d_qtypes[question.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || "") unless option.placeholder
|
|
46
|
+
end
|
|
47
|
+
update_hidden_questions_for(question)
|
|
48
|
+
when :check_box
|
|
49
|
+
d_qtypes[question.key.to_s] = { type: :check_box }
|
|
50
|
+
question.options.each do |option|
|
|
51
|
+
next if option.inner_title
|
|
52
|
+
vars << option.key.to_s
|
|
53
|
+
if question.hidden
|
|
54
|
+
question_titles[option.key.to_s] = strip_tags question.context_free_title
|
|
55
|
+
end
|
|
56
|
+
value = 1
|
|
57
|
+
option_type = { type: :discrete }
|
|
58
|
+
option_type[value.to_s] = (option.context_free_description || "")
|
|
59
|
+
option_type[:depends] = { values: [value, value.to_s].uniq, variable: option.key.to_s } unless options[:without_depends]
|
|
60
|
+
d_qtypes[option.key.to_s] = option_type
|
|
61
|
+
values = [value, value.to_s].uniq
|
|
62
|
+
handle_subquestions(question, question_titles, d_qtypes, vars, option, values, option.key.to_s)
|
|
63
|
+
end
|
|
64
|
+
update_hidden_questions_for(question, for_checkbox: true)
|
|
65
|
+
when :textarea
|
|
66
|
+
d_qtypes[question.key.to_s] = { type: :text_field }
|
|
67
|
+
when :string, :integer, :float
|
|
68
|
+
handle_textfield(question, d_qtypes)
|
|
69
|
+
when :date
|
|
70
|
+
d_qtypes[question.key.to_s] = question.components.each_with_object({ type: :date }) do |component, hash|
|
|
71
|
+
key = question.send("#{component}_key")
|
|
72
|
+
vars << key.to_s
|
|
73
|
+
hash[component] = key.to_s
|
|
74
|
+
end
|
|
75
|
+
when :hidden
|
|
76
|
+
if question.options.blank? # string
|
|
77
|
+
question_titles[question.key.to_s] = strip_tags question.context_free_title
|
|
78
|
+
vars << question.key.to_s unless vars.include? question.key.to_s
|
|
79
|
+
d_qtypes[question.key.to_s] = { type: :text }
|
|
80
|
+
d_qtypes[question.key.to_s][:depends] = :present unless options[:without_depends]
|
|
81
|
+
else
|
|
82
|
+
no_keys = true
|
|
83
|
+
values = []
|
|
84
|
+
question.options.each do |option|
|
|
85
|
+
if option.value # scale or radio
|
|
86
|
+
vars << question.key.to_s unless vars.include? question.key.to_s
|
|
87
|
+
next if option.inner_title
|
|
88
|
+
d_qtypes[question.key.to_s] ||= { type: :scale }
|
|
89
|
+
values << option.value.to_s
|
|
90
|
+
d_qtypes[question.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || "")
|
|
91
|
+
# TODO: missing sub-questions
|
|
92
|
+
else # check_box
|
|
93
|
+
d_qtypes[question.key.to_s] ||= { type: :check_box }
|
|
94
|
+
no_keys = false
|
|
95
|
+
question_titles[option.key.to_s] = strip_tags question.context_free_title
|
|
96
|
+
vars << option.key.to_s
|
|
97
|
+
value = option.value || 1
|
|
98
|
+
option_type = { type: :discrete }
|
|
99
|
+
option_type[value.to_s] = (option.context_free_description || "")
|
|
100
|
+
option_type[:depends] = { values: [value, value.to_s].uniq, variable: option.key.to_s } unless options[:without_depends]
|
|
101
|
+
d_qtypes[option.key.to_s] = option_type
|
|
102
|
+
# TODO: missing sub-questions
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
if no_keys # scale or radio
|
|
106
|
+
d_qtypes[question.key.to_s][:depends] = { values: values, variable: question.key.to_s } unless options[:without_depends]
|
|
107
|
+
question_titles[question.key.to_s] = strip_tags question.context_free_title
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
else
|
|
111
|
+
fail "WARNING: Unimplemented type #{question.type}."
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
update_dqtypes_depends(d_qtypes, question, options)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
seed["quests"] = sort_nested_hash(question_titles)
|
|
118
|
+
seed["d_qtypes"] = sort_nested_hash(d_qtypes)
|
|
119
|
+
seed["name"] = questionnaire.title
|
|
120
|
+
seed["short_description"] = questionnaire.short_description unless questionnaire.short_description.blank?
|
|
121
|
+
seed["description"] = questionnaire.description unless questionnaire.description.blank?
|
|
122
|
+
|
|
123
|
+
# this approach preserves the order of vars as much as possible, adding new vars to the end of the list
|
|
124
|
+
old_vars = (seed["vars"]&.split(",") || []).map(&:to_s)
|
|
125
|
+
new_vars = vars.map(&:to_s)
|
|
126
|
+
seed["vars"] = ((old_vars & new_vars) | new_vars).join(",")
|
|
127
|
+
|
|
128
|
+
scores = process_scores
|
|
129
|
+
|
|
130
|
+
seed["properties"] ||= {}
|
|
131
|
+
# headers outcome (humanized)
|
|
132
|
+
seed["properties"][:score_headers] = scores[:headers]
|
|
133
|
+
# headers data-export
|
|
134
|
+
seed["properties"][:score_keys] = scores[:keys]
|
|
135
|
+
# score names outcome (humanized)
|
|
136
|
+
seed["properties"][:score_labels] = scores[:labels]
|
|
137
|
+
|
|
138
|
+
seed["properties"].merge!(@options[:properties]) if @options.key?(:properties)
|
|
139
|
+
seed["properties"] = sort_nested_hash(seed["properties"])
|
|
140
|
+
|
|
141
|
+
data = {"key" => seed["key"] || options[:roqua_key] || questionnaire.key, "remote_id" => questionnaire.key}
|
|
142
|
+
attrs = %w(name vars quests d_qtypes properties short_description)
|
|
143
|
+
attrs.sort.each do |name|
|
|
144
|
+
data[name] = seed[name]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
data
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def update_hidden_questions_for(question, for_checkbox: false)
|
|
151
|
+
shows = question.options.each_with_object({}) do |option, shows|
|
|
152
|
+
next if option.inner_title
|
|
153
|
+
for key in option.shows_questions
|
|
154
|
+
skey = key.to_s
|
|
155
|
+
if for_checkbox
|
|
156
|
+
# is another checkbox option already showing the target question?
|
|
157
|
+
if shows.key?(skey)
|
|
158
|
+
# then set the target's depends on :present, since we cannot represent depending on multiple variables
|
|
159
|
+
shows[skey] = :present
|
|
160
|
+
else
|
|
161
|
+
shows[skey] = { values: ["1", 1], variable: option.key.to_s }
|
|
162
|
+
end
|
|
163
|
+
else
|
|
164
|
+
shows[skey] ||= { values: [], variable: question.key.to_s }
|
|
165
|
+
shows[skey][:values] |= [option.value.to_s, option.value]
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
for skey, show in shows
|
|
170
|
+
# if a different question is already showing the same question, we cannot register a dependency on both questions
|
|
171
|
+
# (the 'variable' attribute accepts only 1 key). Thus it is better to show the question based on presence of
|
|
172
|
+
# an answer instead of on the depended question's answers.
|
|
173
|
+
if @hidden_questions.has_key?(skey)
|
|
174
|
+
@hidden_questions[skey] = :present
|
|
175
|
+
else
|
|
176
|
+
@hidden_questions[skey] = show
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def update_dqtypes_depends(d_qtypes, question, options)
|
|
182
|
+
if hidden = @hidden_questions[question.key.to_s]
|
|
183
|
+
d_qtypes[question.key.to_s][:depends] ||= hidden unless options[:without_depends]
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def generate_question_titles
|
|
188
|
+
question_titles = {}
|
|
189
|
+
|
|
190
|
+
for question in questions_flat
|
|
191
|
+
unless question.hidden && (question.type == :check_box || question.type == :hidden)
|
|
192
|
+
title = question.context_free_title || question.description || ""
|
|
193
|
+
question_titles[question.key.to_s] = strip_tags(title)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
question_titles
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def questions_flat
|
|
201
|
+
@questions_flat ||= questionnaire.panels.map do |panel|
|
|
202
|
+
panel.items.select { |item| item.is_a? Quby::Compiler::Entities::Question }
|
|
203
|
+
end.flatten.compact
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def handle_subquestions(question, quests, d_qtypes, vars, option, values, key)
|
|
207
|
+
option.questions.each do |quest|
|
|
208
|
+
if quest.presentation == :next_to_title && ![:string, :integer, :float].include?(quest.type)
|
|
209
|
+
fail "unsupported title question type"
|
|
210
|
+
end
|
|
211
|
+
case quest.type
|
|
212
|
+
when :string, :integer, :float
|
|
213
|
+
subquestion(question, quests, d_qtypes, vars, quest, values, key)
|
|
214
|
+
when :textarea
|
|
215
|
+
sub_textfield(question, quests, d_qtypes, vars, quest, values, key)
|
|
216
|
+
when :radio
|
|
217
|
+
sub_radio(question, quests, d_qtypes, vars, quest, values, key)
|
|
218
|
+
when :date
|
|
219
|
+
sub_date(question, quests, d_qtypes, vars, quest, values, key)
|
|
220
|
+
else
|
|
221
|
+
fail "Unimplemented type #{quest.type} for sub_question"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def subquestion(question, quests, d_qtypes, vars, quest, values, key)
|
|
227
|
+
d_qtypes[quest.key.to_s] = { type: :text }
|
|
228
|
+
unless options[:without_depends]
|
|
229
|
+
if quest.presentation == :next_to_title
|
|
230
|
+
# make title questons dependent on themselves so we don't have to dig into quby's depends relations
|
|
231
|
+
# which sometimes refer to some of the parent's options, but not always the correct ones
|
|
232
|
+
d_qtypes[quest.key.to_s][:depends] = :present
|
|
233
|
+
else
|
|
234
|
+
d_qtypes[quest.key.to_s][:depends] = { values: values, variable: key }
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
d_qtypes[quest.key.to_s][:label] = quest.unit unless quest.unit.blank?
|
|
238
|
+
quests[quest.key.to_s] = strip_tags(quest.context_free_title || "")
|
|
239
|
+
vars << quest.key.to_s
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def sub_textfield(question, quests, d_qtypes, vars, quest, values, key)
|
|
243
|
+
d_qtypes[quest.key.to_s] = { type: :text_field }
|
|
244
|
+
d_qtypes[quest.key.to_s][:depends] = { values: values, variable: key } unless options[:without_depends]
|
|
245
|
+
quests[quest.key.to_s] = strip_tags(quest.context_free_title || "")
|
|
246
|
+
vars << quest.key.to_s
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def sub_radio(question, quests, d_qtypes, vars, quest, values, key)
|
|
250
|
+
d_qtypes[quest.key.to_s] = { type: :scale }
|
|
251
|
+
d_qtypes[quest.key.to_s][:depends] = { values: values, variable: key } unless options[:without_depends]
|
|
252
|
+
quests[quest.key.to_s] = strip_tags(quest.context_free_title || "")
|
|
253
|
+
for option in quest.options
|
|
254
|
+
next if option.inner_title
|
|
255
|
+
d_qtypes[quest.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || "")
|
|
256
|
+
end
|
|
257
|
+
vars << quest.key.to_s
|
|
258
|
+
update_hidden_questions_for(quest)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def sub_date(question, quests, d_qtypes, vars, quest, values, key)
|
|
262
|
+
d_qtypes[quest.key.to_s] = quest.components.each_with_object({ type: :date }) do |component, hash|
|
|
263
|
+
key = quest.send("#{component}_key")
|
|
264
|
+
vars << key
|
|
265
|
+
hash[component] = key.to_s
|
|
266
|
+
end
|
|
267
|
+
quests[quest.key.to_s] = strip_tags(quest.context_free_title || "")
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def handle_scale(question, quests, d_qtypes, vars)
|
|
271
|
+
d_qtypes[question.key.to_s] = { type: :scale }
|
|
272
|
+
values = []
|
|
273
|
+
update_hidden_questions_for(question)
|
|
274
|
+
for option in question.options
|
|
275
|
+
next if option.inner_title
|
|
276
|
+
d_qtypes[question.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || "")
|
|
277
|
+
values << option.value.to_s
|
|
278
|
+
key = question.key.to_s
|
|
279
|
+
handle_subquestions(question, quests, d_qtypes, vars, option, [option.value.to_s], key)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def handle_textfield(question, d_qtypes)
|
|
284
|
+
d_qtypes[question.key.to_s] = { type: :text }
|
|
285
|
+
d_qtypes[question.key.to_s][:label] = question.unit unless question.unit.blank?
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def strip_p_tag(text)
|
|
289
|
+
text.gsub(/^<p>(.*)<\/p>\n?$/, "\\1")
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def process_scores
|
|
293
|
+
scores_from_schemas
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def scores_from_schemas
|
|
297
|
+
score_headers = [] # headers outcome (humanized name for subscores)
|
|
298
|
+
score_keys = [] # headers data-export (not all of it, just the score_subscore part, shortened)
|
|
299
|
+
score_labels = [] # score names outcome (humanized name for score as a whole)
|
|
300
|
+
|
|
301
|
+
questionnaire.score_schemas.values.each do |score_schema|
|
|
302
|
+
score_labels << score_schema.label
|
|
303
|
+
score_keys << score_schema.subscore_schemas.map do |subschema|
|
|
304
|
+
hash = {
|
|
305
|
+
key: subschema.key,
|
|
306
|
+
header: subschema.export_key.to_s # a shortened key used as PART OF the csv export column headers
|
|
307
|
+
}
|
|
308
|
+
if subschema.only_for_export
|
|
309
|
+
hash.merge(hidden: true)
|
|
310
|
+
else
|
|
311
|
+
hash
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
headers = score_schema.subscore_schemas.map(&:label)
|
|
316
|
+
score_headers += headers - score_headers
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
{
|
|
320
|
+
headers: score_headers,
|
|
321
|
+
keys: score_keys,
|
|
322
|
+
labels: score_labels
|
|
323
|
+
}
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
class ShortenKeysUniq
|
|
327
|
+
def initialize
|
|
328
|
+
@seen_results = []
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def shorten_one(key)
|
|
332
|
+
key = key.to_s
|
|
333
|
+
limit = 2
|
|
334
|
+
shortened_key = nil
|
|
335
|
+
loop do
|
|
336
|
+
shortened_key = key[0..limit]
|
|
337
|
+
if key[limit] == "_"
|
|
338
|
+
limit += 1
|
|
339
|
+
next
|
|
340
|
+
end
|
|
341
|
+
break unless @seen_results.include?(shortened_key)
|
|
342
|
+
raise "duplicate key, #{key}" if shortened_key.length == key.length
|
|
343
|
+
limit += 1
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
@seen_results << shortened_key
|
|
347
|
+
shortened_key
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def shorten_two(first_key, second_key)
|
|
351
|
+
first_key = first_key.to_s
|
|
352
|
+
second_key = second_key.to_s
|
|
353
|
+
first_limit = [2, first_key.length - 1].min
|
|
354
|
+
second_limit = 0
|
|
355
|
+
shortened_key = nil
|
|
356
|
+
loop do
|
|
357
|
+
shortened_key = "#{first_key[0..first_limit]}_#{second_key[0..second_limit]}"
|
|
358
|
+
if first_key[first_limit] == "_"
|
|
359
|
+
first_limit += 1
|
|
360
|
+
next
|
|
361
|
+
end
|
|
362
|
+
if second_key[second_limit] == "_"
|
|
363
|
+
second_limit += 1
|
|
364
|
+
next
|
|
365
|
+
end
|
|
366
|
+
break unless @seen_results.include?(shortened_key)
|
|
367
|
+
raise "duplicate key, #{first_key}_#{second_key}" if first_limit == (first_key.length - 1) &&
|
|
368
|
+
second_limit == (second_key.length - 1)
|
|
369
|
+
|
|
370
|
+
if second_limit == (second_key.length - 1)
|
|
371
|
+
first_limit += 1
|
|
372
|
+
second_limit = 0
|
|
373
|
+
else
|
|
374
|
+
second_limit += 1
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
@seen_results << shortened_key
|
|
379
|
+
shortened_key
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def sort_nested_hash(obj)
|
|
384
|
+
case obj
|
|
385
|
+
when Hash
|
|
386
|
+
obj.transform_values { |v| sort_nested_hash(v) }
|
|
387
|
+
.sort_by_alphanum { |k, _v| k.to_s }
|
|
388
|
+
.to_h
|
|
389
|
+
when Array
|
|
390
|
+
obj.map { |v| sort_nested_hash(v) }
|
|
391
|
+
else
|
|
392
|
+
obj
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
private
|
|
398
|
+
|
|
399
|
+
def self.keys_for_score(score)
|
|
400
|
+
score.map { |subscore| subscore[:key] }
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module Quby
|
|
2
|
+
module Compiler
|
|
3
|
+
module Services
|
|
4
|
+
class SeedDiff
|
|
5
|
+
def format_patch(reference, candidate, path:)
|
|
6
|
+
@reference_for_debugging = reference
|
|
7
|
+
@candidate_for_debugging = candidate
|
|
8
|
+
format_polymorph_patch(reference, candidate, path: path)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def apply_patch(object, patch)
|
|
12
|
+
return object if patch.nil?
|
|
13
|
+
apply_polymorphic(object, patch, path: "")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def format_polymorph_patch(reference, candidate, path:)
|
|
19
|
+
if reference.class != candidate.class && !candidate.nil? && !depends_relation?(path)
|
|
20
|
+
raise "Incompatible types at #{path}: #{reference.class} and #{candidate.class}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
case reference
|
|
24
|
+
when Hash
|
|
25
|
+
case candidate
|
|
26
|
+
when Hash
|
|
27
|
+
format_hash_patch(reference, candidate, path: path)
|
|
28
|
+
else
|
|
29
|
+
reference
|
|
30
|
+
end
|
|
31
|
+
when Array
|
|
32
|
+
format_array_patch(reference, candidate, path: path)
|
|
33
|
+
when String, Symbol, true, false
|
|
34
|
+
reference
|
|
35
|
+
else
|
|
36
|
+
raise "Cannot patch #{reference.class} yet."
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def format_hash_patch(reference, candidate, path:)
|
|
41
|
+
return reference if candidate.nil?
|
|
42
|
+
|
|
43
|
+
patch = {}
|
|
44
|
+
|
|
45
|
+
reference.keys.each do |key|
|
|
46
|
+
if reference[key] != candidate[key]
|
|
47
|
+
patch[key] = format_polymorph_patch(reference[key], candidate[key], path: path + ".#{key}")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
patch
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def format_array_patch(reference, candidate, path:)
|
|
55
|
+
return reference if candidate.nil?
|
|
56
|
+
|
|
57
|
+
patch = { __patch_type__: "array" }
|
|
58
|
+
|
|
59
|
+
reference.each_with_index do |reference_elm, idx|
|
|
60
|
+
if reference_elm != candidate[idx]
|
|
61
|
+
patch[idx] = reference_elm
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if candidate.size > reference.size
|
|
66
|
+
patch[:__patch_trim__] = reference.size
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
patch
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def apply_polymorphic(object, patch, path:)
|
|
73
|
+
if patch.is_a?(Hash) && patch[:__patch_type__] == "array"
|
|
74
|
+
apply_array(object, patch, path: path)
|
|
75
|
+
elsif patch.is_a?(Hash)
|
|
76
|
+
apply_hash(object, patch, path: path)
|
|
77
|
+
else
|
|
78
|
+
patch
|
|
79
|
+
end
|
|
80
|
+
rescue Exception => e
|
|
81
|
+
puts "path: #{path}"
|
|
82
|
+
raise
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def apply_hash(object, patch, path:)
|
|
86
|
+
object = {} unless object.is_a?(Hash)
|
|
87
|
+
|
|
88
|
+
patch.each do |key, value|
|
|
89
|
+
object[key] = apply_polymorphic(object[key], value, path: path + ".#{key}")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
object
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def apply_array(object, patch, path:)
|
|
96
|
+
object ||= []
|
|
97
|
+
|
|
98
|
+
patch.each do |key, val|
|
|
99
|
+
next if key == :__patch_type__ || key == :__patch_trim__
|
|
100
|
+
object[key] = val
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if patch[:__patch_trim__]
|
|
104
|
+
object = object.slice(0, patch[:__patch_trim__])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
object
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def depends_relation?(path)
|
|
111
|
+
path.end_with?(".depends")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|