# rubocop:disable all require "active_support/all" require 'roqua/core_ext/enumerable/sort_by_alphanum' require 'quby/compiler/services/seed_diff' module Quby module Compiler module Services class QubyProxy HEADERS = { value: "Score", interpretation: "Interpretatie", clin_interp: "Klinisch", norm: "Norm", tscore: "T-Score", dimensie: "Dimensie", mean: "Gemiddelde" } attr_reader :questionnaire, :options def initialize(questionnaire, options) @questionnaire = questionnaire @options = options end 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 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 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 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 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 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 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 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 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 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 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 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 def strip_p_tag(text) text.gsub(/^

(.*)<\/p>\n?$/, '\1') end def strip_question_number_slashes(quests) quests.transform_values! do |value| value&.gsub(/^(\s*\d+)\\/, '\1') end end def process_scores scores_from_schemas end 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| hash = { key: subschema.key, header: subschema.export_key.to_s # a shortened key used as PART OF the csv export column headers } if subschema.only_for_export hash.merge(hidden: true) else hash end end headers = score_schema.subscore_schemas.map(&:label) score_headers += headers - score_headers end { headers: score_headers, keys: score_keys, labels: score_labels } end class ShortenKeysUniq def initialize @seen_results = [] end def shorten_one(key) key = key.to_s limit = 2 shortened_key = nil loop do shortened_key = key[0..limit] if key[limit] == "_" limit += 1 next end break unless @seen_results.include?(shortened_key) raise "duplicate key, #{key}" if shortened_key.length == key.length limit += 1 end @seen_results << shortened_key shortened_key end def shorten_two(first_key, second_key) first_key = first_key.to_s second_key = second_key.to_s first_limit = [2, first_key.length - 1].min second_limit = 0 shortened_key = nil loop do shortened_key = "#{first_key[0..first_limit]}_#{second_key[0..second_limit]}" if first_key[first_limit] == "_" first_limit += 1 next end if second_key[second_limit] == "_" second_limit += 1 next end break unless @seen_results.include?(shortened_key) raise "duplicate key, #{first_key}_#{second_key}" if first_limit == (first_key.length - 1) && second_limit == (second_key.length - 1) if second_limit == (second_key.length - 1) first_limit += 1 second_limit = 0 else second_limit += 1 end end @seen_results << shortened_key shortened_key end end 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 private def self.keys_for_score(score) score.map { |subscore| subscore[:key] } end end end end end