Class: Quby::Compiler::Services::QubyProxy

Inherits:
Object
  • Object
show all
Defined in:
lib/quby/compiler/services/quby_proxy.rb

Defined Under Namespace

Classes: ShortenKeysUniq

Constant Summary collapse

HEADERS =
{ value: "Score",
interpretation: "Interpretatie",
clin_interp: "Klinisch",
norm: "Norm",
tscore: "T-Score",
dimensie: "Dimensie",
mean: "Gemiddelde" }

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(questionnaire, options) ⇒ QubyProxy

Returns a new instance of QubyProxy.



20
21
22
23
# File 'lib/quby/compiler/services/quby_proxy.rb', line 20

def initialize(questionnaire, options)
  @questionnaire = questionnaire
  @options = options
end

Instance Attribute Details

#optionsObject (readonly)

Returns the value of attribute options.



18
19
20
# File 'lib/quby/compiler/services/quby_proxy.rb', line 18

def options
  @options
end

#questionnaireObject (readonly)

Returns the value of attribute questionnaire.



18
19
20
# File 'lib/quby/compiler/services/quby_proxy.rb', line 18

def questionnaire
  @questionnaire
end

Instance Method Details

#generateObject



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/quby/compiler/services/quby_proxy.rb', line 25

def generate
  seed = {}
  question_titles = generate_question_titles
  d_qtypes = {}
  vars = []
  @hidden_questions = {} # hash containing questions hidden by other questions

  for question in questions_flat
    unless question.hidden && (question.type == :check_box || question.type == :hidden)
      vars << question.key.to_s
    end

    case question.type
    when :radio, :scale
      handle_scale(question, question_titles, d_qtypes, vars)
    when :select
      d_qtypes[question.key.to_s] = { type: :discrete }
      for option in question.options
        d_qtypes[question.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || option.description || "") unless option.placeholder
      end
      update_hidden_questions_for(question)
    when :check_box
      d_qtypes[question.key.to_s] = { type: :check_box }
      question.options.each do |option|
        next if option.inner_title
        vars << option.key.to_s
        if question.hidden
          question_titles[option.key.to_s] = strip_tags(question.context_free_title_or_title)
        end
        value = 1
        option_type = { type: :discrete }
        option_type[value.to_s] = (option.context_free_description || option.description || "")
        option_type[:depends] = { values: [value, value.to_s].uniq, variable: option.key.to_s } unless options[:without_depends]
        d_qtypes[option.key.to_s] = option_type
        values = [value, value.to_s].uniq
        handle_subquestions(question, question_titles, d_qtypes, vars, option, values, option.key.to_s)
      end
      if question.title_question
        subquestion(question, question_titles, d_qtypes, vars, question.title_question, nil, nil)
      end
      update_hidden_questions_for(question, for_checkbox: true)
    when :textarea
      d_qtypes[question.key.to_s] = { type: :text_field }
    when :string, :integer, :float
      handle_textfield(question, d_qtypes)
    when :date
      d_qtypes[question.key.to_s] = question.components.each_with_object({ type: :date }) do |component, hash|
        key = question.send("#{component}_key")
        vars << key.to_s
        hash[component] = key.to_s
      end
    when :hidden
      if question.options.blank? # string
        question_titles[question.key.to_s] = strip_tags(question.context_free_title_or_title)
        vars << question.key.to_s unless vars.include? question.key.to_s
        d_qtypes[question.key.to_s] = { type: :text }
        d_qtypes[question.key.to_s][:depends] = :present unless options[:without_depends]
      else
        no_keys = true
        values = []
        question.options.each do |option|
          if option.value # scale or radio
            vars << question.key.to_s unless vars.include? question.key.to_s
            next if option.inner_title
            d_qtypes[question.key.to_s] ||= { type: :scale }
            values << option.value.to_s
            d_qtypes[question.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || option.description || "")
            # TODO: missing sub-questions
          else # check_box
            d_qtypes[question.key.to_s] ||= { type: :check_box }
            no_keys = false
            question_titles[option.key.to_s] = strip_tags(question.context_free_title_or_title)
            vars << option.key.to_s
            value = option.value || 1
            option_type = { type: :discrete }
            option_type[value.to_s] = (option.context_free_description || option.description || "")
            option_type[:depends] = { values: [value, value.to_s].uniq, variable: option.key.to_s } unless options[:without_depends]
            d_qtypes[option.key.to_s] = option_type
            # TODO: missing sub-questions
          end
        end
        if no_keys # scale or radio
          d_qtypes[question.key.to_s][:depends] = { values: values, variable: question.key.to_s } unless options[:without_depends]
          question_titles[question.key.to_s] = strip_tags(question.context_free_title_or_title)
        end
      end
    else
      fail "WARNING: Unimplemented type #{question.type}."
    end

    update_dqtypes_depends(d_qtypes, question, options)
  end

  strip_question_number_slashes(question_titles)
  seed["quests"] = sort_nested_hash(question_titles)
  seed["d_qtypes"] = sort_nested_hash(d_qtypes)
  seed["name"] = questionnaire.title
  seed["short_description"] = questionnaire.short_description unless questionnaire.short_description.blank?
  seed["description"] = questionnaire.description unless questionnaire.description.blank?

  # this approach preserves the order of vars as much as possible, adding new vars to the end of the list
  old_vars = (seed["vars"]&.split(",") || []).map(&:to_s)
  new_vars = vars.map(&:to_s)
  seed["vars"] = ((old_vars & new_vars) | new_vars).join(",")

  scores = process_scores

  seed["properties"] ||= {}
  # headers outcome (humanized)
  seed["properties"][:score_headers] = scores[:headers]
  # headers data-export
  seed["properties"][:score_keys] = scores[:keys]
  # score names outcome (humanized)
  seed["properties"][:score_labels] = scores[:labels]

  seed["properties"].merge!(@options[:properties]) if @options.key?(:properties)
  seed["properties"] = sort_nested_hash(seed["properties"])

  data = {"key" => seed["key"] || options[:roqua_key] || questionnaire.key, "remote_id" => questionnaire.key}
  attrs = %w(name vars quests d_qtypes properties short_description)
  attrs.sort.each do |name|
    data[name] = seed[name]
  end

  data
end

#generate_question_titlesObject



192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/quby/compiler/services/quby_proxy.rb', line 192

def generate_question_titles
  question_titles = {}

  for question in questions_flat
    unless question.hidden && (question.type == :check_box || question.type == :hidden)
      title = question.context_free_title_or_title || question.description || ""
      question_titles[question.key.to_s] = strip_tags(title)
    end
  end

  question_titles
end

#handle_scale(question, quests, d_qtypes, vars) ⇒ Object



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/quby/compiler/services/quby_proxy.rb', line 275

def handle_scale(question, quests, d_qtypes, vars)
  d_qtypes[question.key.to_s] = { type: :scale }
  values = []
  update_hidden_questions_for(question)
  for option in question.options
    next if option.inner_title
    d_qtypes[question.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || option.description || "")
    values << option.value.to_s
    key = question.key.to_s
    handle_subquestions(question, quests, d_qtypes, vars, option, [option.value.to_s], key)
  end
  if question.title_question
    subquestion(question, quests, d_qtypes, vars, question.title_question, nil, nil)
  end
end

#handle_subquestions(question, quests, d_qtypes, vars, option, values, key) ⇒ Object



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/quby/compiler/services/quby_proxy.rb', line 211

def handle_subquestions(question, quests, d_qtypes, vars, option, values, key)
  option.questions.each do |quest|
    if quest.presentation == :next_to_title && ![:string, :integer, :float].include?(quest.type)
      fail "unsupported title question type"
    end
    case quest.type
    when :string, :integer, :float
      subquestion(question, quests, d_qtypes, vars, quest, values, key)
    when :textarea
      sub_textfield(question, quests, d_qtypes, vars, quest, values, key)
    when :radio
      sub_radio(question, quests, d_qtypes, vars, quest, values, key)
    when :date
      sub_date(question, quests, d_qtypes, vars, quest, values, key)
    else
      fail "Unimplemented type #{quest.type} for sub_question"
    end
  end
end

#handle_textfield(question, d_qtypes) ⇒ Object



291
292
293
294
# File 'lib/quby/compiler/services/quby_proxy.rb', line 291

def handle_textfield(question, d_qtypes)
  d_qtypes[question.key.to_s] = { type: :text }
  d_qtypes[question.key.to_s][:label] = question.unit unless question.unit.blank?
end

#process_scoresObject



306
307
308
# File 'lib/quby/compiler/services/quby_proxy.rb', line 306

def process_scores
  scores_from_schemas
end

#questions_flatObject



205
206
207
208
209
# File 'lib/quby/compiler/services/quby_proxy.rb', line 205

def questions_flat
  @questions_flat ||= questionnaire.panels.map do |panel|
    panel.items.select { |item| item.is_a? Quby::Compiler::Entities::Question }
  end.flatten.compact
end

#scores_from_schemasObject



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/quby/compiler/services/quby_proxy.rb', line 310

def scores_from_schemas
  score_headers = [] # headers outcome (humanized name for subscores)
  score_keys    = [] # headers data-export (not all of it, just the score_subscore part, shortened)
  score_labels  = [] # score names outcome (humanized name for score as a whole)

  questionnaire.score_schemas.values.each do |score_schema|
    score_labels << score_schema.label
    score_keys << score_schema.subscore_schemas.map do |subschema|
      {
        key: subschema.key,
        header: subschema.export_key.to_s # a shortened key used as PART OF the csv export column headers
      }
    end

    headers = score_schema.subscore_schemas.map(&:label)
    score_headers += headers - score_headers
  end

  {
    headers: score_headers,
    keys: score_keys,
    labels: score_labels
  }
end

#sort_nested_hash(obj) ⇒ Object



392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/quby/compiler/services/quby_proxy.rb', line 392

def sort_nested_hash(obj)
  case obj
  when Hash
    obj.transform_values { |v| sort_nested_hash(v) }
       .sort_by_alphanum { |k, _v| k.to_s }
       .to_h
  when Array
    obj.map { |v| sort_nested_hash(v) }
  else
    obj
  end
end

#strip_p_tag(text) ⇒ Object



296
297
298
# File 'lib/quby/compiler/services/quby_proxy.rb', line 296

def strip_p_tag(text)
  text.gsub(/^<p>(.*)<\/p>\n?$/, '\1')
end

#strip_question_number_slashes(quests) ⇒ Object



300
301
302
303
304
# File 'lib/quby/compiler/services/quby_proxy.rb', line 300

def strip_question_number_slashes(quests)
  quests.transform_values! do |value|
    value&.gsub(/^(\s*\d+)\\/, '\1')
  end
end

#sub_date(question, quests, d_qtypes, vars, quest, values, key) ⇒ Object



266
267
268
269
270
271
272
273
# File 'lib/quby/compiler/services/quby_proxy.rb', line 266

def sub_date(question, quests, d_qtypes, vars, quest, values, key)
  d_qtypes[quest.key.to_s] = quest.components.each_with_object({ type: :date }) do |component, hash|
    key = quest.send("#{component}_key")
    vars << key
    hash[component] = key.to_s
  end
  quests[quest.key.to_s] = strip_tags(quest.context_free_title || quest.title || "")
end

#sub_radio(question, quests, d_qtypes, vars, quest, values, key) ⇒ Object



254
255
256
257
258
259
260
261
262
263
264
# File 'lib/quby/compiler/services/quby_proxy.rb', line 254

def sub_radio(question, quests, d_qtypes, vars, quest, values, key)
  d_qtypes[quest.key.to_s] = { type: :scale }
  d_qtypes[quest.key.to_s][:depends] = { values: values, variable: key } unless options[:without_depends]
  quests[quest.key.to_s] = strip_tags(quest.context_free_title || quest.title || "")
  for option in quest.options
    next if option.inner_title
    d_qtypes[quest.key.to_s][option.value.to_s] = strip_p_tag(option.context_free_description || option.description || "")
  end
  vars << quest.key.to_s
  update_hidden_questions_for(quest)
end

#sub_textfield(question, quests, d_qtypes, vars, quest, values, key) ⇒ Object



247
248
249
250
251
252
# File 'lib/quby/compiler/services/quby_proxy.rb', line 247

def sub_textfield(question, quests, d_qtypes, vars, quest, values, key)
  d_qtypes[quest.key.to_s] = { type: :text_field }
  d_qtypes[quest.key.to_s][:depends] = { values: values, variable: key } unless options[:without_depends]
  quests[quest.key.to_s] = strip_tags(quest.context_free_title || quest.title || "")
  vars << quest.key.to_s
end

#subquestion(question, quests, d_qtypes, vars, quest, values, key) ⇒ Object



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/quby/compiler/services/quby_proxy.rb', line 231

def subquestion(question, quests, d_qtypes, vars, quest, values, key)
  d_qtypes[quest.key.to_s] = { type: :text }
  unless options[:without_depends]
    if quest.presentation == :next_to_title
      # make title questons dependent on themselves so we don't have to dig into quby's depends relations
      # which sometimes refer to some of the parent's options, but not always the correct ones
      d_qtypes[quest.key.to_s][:depends] = :present
    else
      d_qtypes[quest.key.to_s][:depends] = { values: values, variable: key }
    end
  end
  d_qtypes[quest.key.to_s][:label] = quest.unit unless quest.unit.blank?
  quests[quest.key.to_s] = strip_tags(quest.context_free_title || quest.title || "")
  vars << quest.key.to_s
end

#update_dqtypes_depends(d_qtypes, question, options) ⇒ Object



183
184
185
186
187
188
189
190
# File 'lib/quby/compiler/services/quby_proxy.rb', line 183

def update_dqtypes_depends(d_qtypes, question, options)
  if hidden = @hidden_questions[question.key.to_s]
    d_qtypes[question.key.to_s][:depends] ||= hidden unless options[:without_depends]
  end
  if question.hidden && question.type != :check_box
    d_qtypes[question.key.to_s][:depends] = :present unless options[:without_depends]
  end
end

#update_hidden_questions_for(question, for_checkbox: false) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/quby/compiler/services/quby_proxy.rb', line 152

def update_hidden_questions_for(question, for_checkbox: false)
  shows = question.options.each_with_object({}) do |option, shows|
    next if option.inner_title
    for key in option.shows_questions
      skey = key.to_s
      if for_checkbox
        # is another checkbox option already showing the target question?
        if shows.key?(skey)
          # then set the target's depends on :present, since we cannot represent depending on multiple variables
          shows[skey] = :present
        else
          shows[skey] = { values: ["1", 1], variable: option.key.to_s }
        end
      else
        shows[skey] ||= { values: [], variable: question.key.to_s }
        shows[skey][:values] |= [option.value.to_s, option.value]
      end
    end
  end
  for skey, show in shows
    # if a different question is already showing the same question, we cannot register a dependency on both questions
    # (the 'variable' attribute accepts only 1 key). Thus it is better to show the question based on presence of
    # an answer instead of on the depended question's answers.
    if @hidden_questions.has_key?(skey)
      @hidden_questions[skey] = :present
    else
      @hidden_questions[skey] = show
    end
  end
end